Jelajahi Sumber

Fix Schedule and Join Meetings layout and scroll behavior

- Activate empty-state width constraint after adding the view to the stack to avoid NSLayoutException.
- Deactivate and replace main content host edge constraints on each sidebar swap so constraints do not accumulate.
- Schedule cards NSScrollView: avoid stretching the document stack to the clip height; use a flipped NSClipView so short content stays top-aligned instead of bottom-anchored with a large gap above.
- Join Meetings: tighten spacing from the schedule header and date line to the meeting card strip; disable automatic content insets and drop the document bottom pin on the horizontal strip scroll view.

Made-with: Cursor
huzaifahayat12 3 bulan lalu
induk
melakukan
5b1f47985e
1 mengubah file dengan 42 tambahan dan 9 penghapusan
  1. 42 9
      meetings_app/ViewController.swift

+ 42 - 9
meetings_app/ViewController.swift

@@ -236,6 +236,8 @@ final class ViewController: NSViewController {
     private let launchMinContentSize = NSSize(width: 760, height: 600)
 
     private var mainContentHost: NSView?
+    /// Pin constraints for the current page inside `mainContentHost`; deactivated before each swap so relayout never stacks duplicates.
+    private var mainContentHostPinConstraints: [NSLayoutConstraint] = []
     private var sidebarRowViews: [SidebarPage: NSView] = [:]
     private var selectedSidebarPage: SidebarPage = .joinMeetings
     private var selectedZoomJoinMode: ZoomJoinMode = .id
@@ -332,7 +334,13 @@ final class ViewController: NSViewController {
     private let schedulePageCardsPerRow: Int = 3
     private let schedulePageCardSpacing: CGFloat = 20
     private let schedulePageCardHeight: CGFloat = 182
-    private let schedulePageStackSpacing: CGFloat = 16
+    /// Match `makeJoinMeetingsContent` vertical rhythm between sections.
+    private let schedulePageStackSpacing: CGFloat = 14
+    /// Tighter gap from header block (title + filters) to the date line below.
+    private let schedulePageHeaderToDateSpacing: CGFloat = 10
+    /// Join Meetings: gap from “Schedule” row to date heading, and date heading to card strip (keeps cards aligned with the rest of the column).
+    private let joinPageScheduleHeaderToDateSpacing: CGFloat = 8
+    private let joinPageDateToMeetingCardsSpacing: CGFloat = 8
     /// Match Join Meetings main content insets so the top auth/profile bar lines up with page edges.
     private let schedulePageLeadingInset: CGFloat = 28
     private let schedulePageTrailingInset: CGFloat = 28
@@ -730,16 +738,19 @@ private extension ViewController {
         applyWindowTitle(for: page)
 
         guard let host = mainContentHost else { return }
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
+        mainContentHostPinConstraints.removeAll()
         host.subviews.forEach { $0.removeFromSuperview() }
         let child = viewForPage(page)
         child.translatesAutoresizingMaskIntoConstraints = false
         host.addSubview(child)
-        NSLayoutConstraint.activate([
+        mainContentHostPinConstraints = [
             child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
             child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
             child.topAnchor.constraint(equalTo: host.topAnchor),
             child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
-        ])
+        ]
+        NSLayoutConstraint.activate(mainContentHostPinConstraints)
     }
 
     private func showSettingsPopover() {
@@ -789,6 +800,8 @@ private extension ViewController {
         googleAccountPopover?.performClose(nil)
         googleAccountPopover = nil
 
+        NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
+        mainContentHostPinConstraints.removeAll()
         mainContentHost = nil
         view.subviews.forEach { $0.removeFromSuperview() }
         setupRootView()
@@ -1713,11 +1726,14 @@ private extension ViewController {
         contentStack.addArrangedSubview(meetJoinSectionRow())
         contentStack.addArrangedSubview(joinActions)
         contentStack.setCustomSpacing(26, after: joinActions)
-        contentStack.addArrangedSubview(scheduleHeader())
+        let scheduleHeaderView = scheduleHeader()
+        contentStack.addArrangedSubview(scheduleHeaderView)
+        contentStack.setCustomSpacing(joinPageScheduleHeaderToDateSpacing, after: scheduleHeaderView)
 
         let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
         scheduleDateHeadingLabel = dateHeading
         contentStack.addArrangedSubview(dateHeading)
+        contentStack.setCustomSpacing(joinPageDateToMeetingCardsSpacing, after: dateHeading)
 
         let cardsRow = scheduleCardsRow(meetings: [])
         contentStack.addArrangedSubview(cardsRow)
@@ -1748,22 +1764,31 @@ private extension ViewController {
         contentStack.orientation = .vertical
         contentStack.spacing = schedulePageStackSpacing
         contentStack.alignment = .width
+        contentStack.distribution = .fill
 
         let header = schedulePageHeader()
+        header.setContentHuggingPriority(.required, for: .vertical)
+        header.setContentCompressionResistancePriority(.required, for: .vertical)
         contentStack.addArrangedSubview(header)
+        contentStack.setCustomSpacing(schedulePageHeaderToDateSpacing, after: header)
 
         let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
         heading.alignment = .left
+        heading.setContentHuggingPriority(.required, for: .vertical)
+        heading.setContentCompressionResistancePriority(.required, for: .vertical)
         schedulePageDateHeadingLabel = heading
         contentStack.addArrangedSubview(heading)
 
         let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
         rangeError.alignment = .left
         rangeError.isHidden = true
+        rangeError.setContentHuggingPriority(.required, for: .vertical)
+        rangeError.setContentCompressionResistancePriority(.required, for: .vertical)
         schedulePageRangeErrorLabel = rangeError
         contentStack.addArrangedSubview(rangeError)
 
         let cardsContainer = makeSchedulePageCardsContainer()
+        cardsContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
         contentStack.addArrangedSubview(cardsContainer)
 
         panel.addSubview(contentStack)
@@ -2672,7 +2697,7 @@ private extension ViewController {
         container.translatesAutoresizingMaskIntoConstraints = false
         container.userInterfaceLayoutDirection = .leftToRight
         container.orientation = .vertical
-        container.spacing = 14
+        container.spacing = 8
         container.alignment = .width
 
         let titleRow = NSStackView()
@@ -2832,6 +2857,10 @@ private extension ViewController {
         scroll.borderType = .noBorder
         scroll.scrollerStyle = .overlay
         scroll.automaticallyAdjustsContentInsets = false
+        let clip = TopAlignedClipView()
+        clip.drawsBackground = false
+        clip.postsBoundsChangedNotifications = true
+        scroll.contentView = clip
         schedulePageCardsScrollView = scroll
         wrapper.addSubview(scroll)
 
@@ -2854,7 +2883,6 @@ private extension ViewController {
             stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
             stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
             stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
-            stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
             stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
         ])
 
@@ -3058,6 +3086,7 @@ private extension ViewController {
         scroll.verticalScrollElasticity = .none
         scroll.autohidesScrollers = false
         scroll.borderType = .noBorder
+        scroll.automaticallyAdjustsContentInsets = false
         scroll.heightAnchor.constraint(equalToConstant: 150).isActive = true
         scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
 
@@ -3074,12 +3103,11 @@ private extension ViewController {
         scroll.documentView = row
         scroll.contentView.postsBoundsChangedNotifications = true
 
-        // Ensure the stack view determines content size for horizontal scrolling.
+        // Pin top/leading/trailing only; avoid bottom == clip so the horizontal stack is not stretched vertically inside the clip (same as Schedule cards scroll).
         NSLayoutConstraint.activate([
             row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
             row.trailingAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.trailingAnchor),
             row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
-            row.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
             row.heightAnchor.constraint(equalToConstant: 150)
         ])
 
@@ -3300,6 +3328,11 @@ extension ViewController: NSWindowDelegate {
     }
 }
 
+/// Default `NSClipView` uses a non-flipped coordinate system, so a document shorter than the visible area is anchored to the **bottom** of the clip, leaving a large gap above (e.g. Schedule empty state). Flipped coordinates match Auto Layout’s top-leading anchors and keep content top-aligned.
+private final class TopAlignedClipView: NSClipView {
+    override var isFlipped: Bool { true }
+}
+
 /// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
 private final class GoogleProfileAuthHostView: NSView {
     weak var authButton: NSButton? {
@@ -4510,7 +4543,6 @@ private extension ViewController {
             let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
             empty.translatesAutoresizingMaskIntoConstraints = false
             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
@@ -4520,6 +4552,7 @@ private extension ViewController {
                 label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
             ])
             stack.addArrangedSubview(empty)
+            empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
             return
         }