|
|
@@ -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
|
|
|
}
|
|
|
|