|
|
@@ -236,6 +236,8 @@ final class ViewController: NSViewController {
|
|
236
|
236
|
private let launchMinContentSize = NSSize(width: 760, height: 600)
|
|
237
|
237
|
|
|
238
|
238
|
private var mainContentHost: NSView?
|
|
|
239
|
+ /// Pin constraints for the current page inside `mainContentHost`; deactivated before each swap so relayout never stacks duplicates.
|
|
|
240
|
+ private var mainContentHostPinConstraints: [NSLayoutConstraint] = []
|
|
239
|
241
|
private var sidebarRowViews: [SidebarPage: NSView] = [:]
|
|
240
|
242
|
private var selectedSidebarPage: SidebarPage = .joinMeetings
|
|
241
|
243
|
private var selectedZoomJoinMode: ZoomJoinMode = .id
|
|
|
@@ -332,7 +334,13 @@ final class ViewController: NSViewController {
|
|
332
|
334
|
private let schedulePageCardsPerRow: Int = 3
|
|
333
|
335
|
private let schedulePageCardSpacing: CGFloat = 20
|
|
334
|
336
|
private let schedulePageCardHeight: CGFloat = 182
|
|
335
|
|
- private let schedulePageStackSpacing: CGFloat = 16
|
|
|
337
|
+ /// Match `makeJoinMeetingsContent` vertical rhythm between sections.
|
|
|
338
|
+ private let schedulePageStackSpacing: CGFloat = 14
|
|
|
339
|
+ /// Tighter gap from header block (title + filters) to the date line below.
|
|
|
340
|
+ private let schedulePageHeaderToDateSpacing: CGFloat = 10
|
|
|
341
|
+ /// Join Meetings: gap from “Schedule” row to date heading, and date heading to card strip (keeps cards aligned with the rest of the column).
|
|
|
342
|
+ private let joinPageScheduleHeaderToDateSpacing: CGFloat = 8
|
|
|
343
|
+ private let joinPageDateToMeetingCardsSpacing: CGFloat = 8
|
|
336
|
344
|
/// Match Join Meetings main content insets so the top auth/profile bar lines up with page edges.
|
|
337
|
345
|
private let schedulePageLeadingInset: CGFloat = 28
|
|
338
|
346
|
private let schedulePageTrailingInset: CGFloat = 28
|
|
|
@@ -730,16 +738,19 @@ private extension ViewController {
|
|
730
|
738
|
applyWindowTitle(for: page)
|
|
731
|
739
|
|
|
732
|
740
|
guard let host = mainContentHost else { return }
|
|
|
741
|
+ NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
|
|
|
742
|
+ mainContentHostPinConstraints.removeAll()
|
|
733
|
743
|
host.subviews.forEach { $0.removeFromSuperview() }
|
|
734
|
744
|
let child = viewForPage(page)
|
|
735
|
745
|
child.translatesAutoresizingMaskIntoConstraints = false
|
|
736
|
746
|
host.addSubview(child)
|
|
737
|
|
- NSLayoutConstraint.activate([
|
|
|
747
|
+ mainContentHostPinConstraints = [
|
|
738
|
748
|
child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
|
739
|
749
|
child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
|
740
|
750
|
child.topAnchor.constraint(equalTo: host.topAnchor),
|
|
741
|
751
|
child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
|
|
742
|
|
- ])
|
|
|
752
|
+ ]
|
|
|
753
|
+ NSLayoutConstraint.activate(mainContentHostPinConstraints)
|
|
743
|
754
|
}
|
|
744
|
755
|
|
|
745
|
756
|
private func showSettingsPopover() {
|
|
|
@@ -789,6 +800,8 @@ private extension ViewController {
|
|
789
|
800
|
googleAccountPopover?.performClose(nil)
|
|
790
|
801
|
googleAccountPopover = nil
|
|
791
|
802
|
|
|
|
803
|
+ NSLayoutConstraint.deactivate(mainContentHostPinConstraints)
|
|
|
804
|
+ mainContentHostPinConstraints.removeAll()
|
|
792
|
805
|
mainContentHost = nil
|
|
793
|
806
|
view.subviews.forEach { $0.removeFromSuperview() }
|
|
794
|
807
|
setupRootView()
|
|
|
@@ -1713,11 +1726,14 @@ private extension ViewController {
|
|
1713
|
1726
|
contentStack.addArrangedSubview(meetJoinSectionRow())
|
|
1714
|
1727
|
contentStack.addArrangedSubview(joinActions)
|
|
1715
|
1728
|
contentStack.setCustomSpacing(26, after: joinActions)
|
|
1716
|
|
- contentStack.addArrangedSubview(scheduleHeader())
|
|
|
1729
|
+ let scheduleHeaderView = scheduleHeader()
|
|
|
1730
|
+ contentStack.addArrangedSubview(scheduleHeaderView)
|
|
|
1731
|
+ contentStack.setCustomSpacing(joinPageScheduleHeaderToDateSpacing, after: scheduleHeaderView)
|
|
1717
|
1732
|
|
|
1718
|
1733
|
let dateHeading = textLabel(scheduleInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
|
|
1719
|
1734
|
scheduleDateHeadingLabel = dateHeading
|
|
1720
|
1735
|
contentStack.addArrangedSubview(dateHeading)
|
|
|
1736
|
+ contentStack.setCustomSpacing(joinPageDateToMeetingCardsSpacing, after: dateHeading)
|
|
1721
|
1737
|
|
|
1722
|
1738
|
let cardsRow = scheduleCardsRow(meetings: [])
|
|
1723
|
1739
|
contentStack.addArrangedSubview(cardsRow)
|
|
|
@@ -1748,22 +1764,31 @@ private extension ViewController {
|
|
1748
|
1764
|
contentStack.orientation = .vertical
|
|
1749
|
1765
|
contentStack.spacing = schedulePageStackSpacing
|
|
1750
|
1766
|
contentStack.alignment = .width
|
|
|
1767
|
+ contentStack.distribution = .fill
|
|
1751
|
1768
|
|
|
1752
|
1769
|
let header = schedulePageHeader()
|
|
|
1770
|
+ header.setContentHuggingPriority(.required, for: .vertical)
|
|
|
1771
|
+ header.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
1753
|
1772
|
contentStack.addArrangedSubview(header)
|
|
|
1773
|
+ contentStack.setCustomSpacing(schedulePageHeaderToDateSpacing, after: header)
|
|
1754
|
1774
|
|
|
1755
|
1775
|
let heading = textLabel(schedulePageInitialHeadingText(), font: typography.dateHeading, color: palette.textSecondary)
|
|
1756
|
1776
|
heading.alignment = .left
|
|
|
1777
|
+ heading.setContentHuggingPriority(.required, for: .vertical)
|
|
|
1778
|
+ heading.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
1757
|
1779
|
schedulePageDateHeadingLabel = heading
|
|
1758
|
1780
|
contentStack.addArrangedSubview(heading)
|
|
1759
|
1781
|
|
|
1760
|
1782
|
let rangeError = textLabel("", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .systemRed)
|
|
1761
|
1783
|
rangeError.alignment = .left
|
|
1762
|
1784
|
rangeError.isHidden = true
|
|
|
1785
|
+ rangeError.setContentHuggingPriority(.required, for: .vertical)
|
|
|
1786
|
+ rangeError.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
1763
|
1787
|
schedulePageRangeErrorLabel = rangeError
|
|
1764
|
1788
|
contentStack.addArrangedSubview(rangeError)
|
|
1765
|
1789
|
|
|
1766
|
1790
|
let cardsContainer = makeSchedulePageCardsContainer()
|
|
|
1791
|
+ cardsContainer.setContentHuggingPriority(.defaultLow, for: .vertical)
|
|
1767
|
1792
|
contentStack.addArrangedSubview(cardsContainer)
|
|
1768
|
1793
|
|
|
1769
|
1794
|
panel.addSubview(contentStack)
|
|
|
@@ -2672,7 +2697,7 @@ private extension ViewController {
|
|
2672
|
2697
|
container.translatesAutoresizingMaskIntoConstraints = false
|
|
2673
|
2698
|
container.userInterfaceLayoutDirection = .leftToRight
|
|
2674
|
2699
|
container.orientation = .vertical
|
|
2675
|
|
- container.spacing = 14
|
|
|
2700
|
+ container.spacing = 8
|
|
2676
|
2701
|
container.alignment = .width
|
|
2677
|
2702
|
|
|
2678
|
2703
|
let titleRow = NSStackView()
|
|
|
@@ -2832,6 +2857,10 @@ private extension ViewController {
|
|
2832
|
2857
|
scroll.borderType = .noBorder
|
|
2833
|
2858
|
scroll.scrollerStyle = .overlay
|
|
2834
|
2859
|
scroll.automaticallyAdjustsContentInsets = false
|
|
|
2860
|
+ let clip = TopAlignedClipView()
|
|
|
2861
|
+ clip.drawsBackground = false
|
|
|
2862
|
+ clip.postsBoundsChangedNotifications = true
|
|
|
2863
|
+ scroll.contentView = clip
|
|
2835
|
2864
|
schedulePageCardsScrollView = scroll
|
|
2836
|
2865
|
wrapper.addSubview(scroll)
|
|
2837
|
2866
|
|
|
|
@@ -2854,7 +2883,6 @@ private extension ViewController {
|
|
2854
|
2883
|
stack.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
2855
|
2884
|
stack.trailingAnchor.constraint(equalTo: scroll.contentView.trailingAnchor),
|
|
2856
|
2885
|
stack.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
2857
|
|
- stack.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
|
|
2858
|
2886
|
stack.widthAnchor.constraint(equalTo: scroll.contentView.widthAnchor)
|
|
2859
|
2887
|
])
|
|
2860
|
2888
|
|
|
|
@@ -3058,6 +3086,7 @@ private extension ViewController {
|
|
3058
|
3086
|
scroll.verticalScrollElasticity = .none
|
|
3059
|
3087
|
scroll.autohidesScrollers = false
|
|
3060
|
3088
|
scroll.borderType = .noBorder
|
|
|
3089
|
+ scroll.automaticallyAdjustsContentInsets = false
|
|
3061
|
3090
|
scroll.heightAnchor.constraint(equalToConstant: 150).isActive = true
|
|
3062
|
3091
|
scroll.widthAnchor.constraint(equalToConstant: viewportWidth).isActive = true
|
|
3063
|
3092
|
|
|
|
@@ -3074,12 +3103,11 @@ private extension ViewController {
|
|
3074
|
3103
|
scroll.documentView = row
|
|
3075
|
3104
|
scroll.contentView.postsBoundsChangedNotifications = true
|
|
3076
|
3105
|
|
|
3077
|
|
- // Ensure the stack view determines content size for horizontal scrolling.
|
|
|
3106
|
+ // Pin top/leading/trailing only; avoid bottom == clip so the horizontal stack is not stretched vertically inside the clip (same as Schedule cards scroll).
|
|
3078
|
3107
|
NSLayoutConstraint.activate([
|
|
3079
|
3108
|
row.leadingAnchor.constraint(equalTo: scroll.contentView.leadingAnchor),
|
|
3080
|
3109
|
row.trailingAnchor.constraint(greaterThanOrEqualTo: scroll.contentView.trailingAnchor),
|
|
3081
|
3110
|
row.topAnchor.constraint(equalTo: scroll.contentView.topAnchor),
|
|
3082
|
|
- row.bottomAnchor.constraint(equalTo: scroll.contentView.bottomAnchor),
|
|
3083
|
3111
|
row.heightAnchor.constraint(equalToConstant: 150)
|
|
3084
|
3112
|
])
|
|
3085
|
3113
|
|
|
|
@@ -3300,6 +3328,11 @@ extension ViewController: NSWindowDelegate {
|
|
3300
|
3328
|
}
|
|
3301
|
3329
|
}
|
|
3302
|
3330
|
|
|
|
3331
|
+/// 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.
|
|
|
3332
|
+private final class TopAlignedClipView: NSClipView {
|
|
|
3333
|
+ override var isFlipped: Bool { true }
|
|
|
3334
|
+}
|
|
|
3335
|
+
|
|
3303
|
3336
|
/// Wraps the Google auth control and draws a circular accent ring with a light pulse while the signed-in avatar is hovered.
|
|
3304
|
3337
|
private final class GoogleProfileAuthHostView: NSView {
|
|
3305
|
3338
|
weak var authButton: NSButton? {
|
|
|
@@ -4510,7 +4543,6 @@ private extension ViewController {
|
|
4510
|
4543
|
let empty = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
|
|
4511
|
4544
|
empty.translatesAutoresizingMaskIntoConstraints = false
|
|
4512
|
4545
|
empty.heightAnchor.constraint(equalToConstant: 140).isActive = true
|
|
4513
|
|
- empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
4514
|
4546
|
styleSurface(empty, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
|
|
4515
|
4547
|
let label = textLabel(googleOAuth.loadTokens() == nil ? "Connect to load schedule" : "No meetings for selected filters", font: typography.cardSubtitle, color: palette.textSecondary)
|
|
4516
|
4548
|
label.translatesAutoresizingMaskIntoConstraints = false
|
|
|
@@ -4520,6 +4552,7 @@ private extension ViewController {
|
|
4520
|
4552
|
label.centerYAnchor.constraint(equalTo: empty.centerYAnchor)
|
|
4521
|
4553
|
])
|
|
4522
|
4554
|
stack.addArrangedSubview(empty)
|
|
|
4555
|
+ empty.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
|
|
4523
|
4556
|
return
|
|
4524
|
4557
|
}
|
|
4525
|
4558
|
|