| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187 |
- //
- // ViewController.swift
- // meetings_app
- //
- // Created by Dev Mac 1 on 06/04/2026.
- //
- import Cocoa
- private enum SidebarPage: Int {
- case joinMeetings = 0
- case photo = 1
- case video = 2
- case tutorials = 3
- case settings = 4
- }
- private enum MeetingProvider: Int {
- case meet = 0
- case zoom = 1
- case teams = 2
- case zoho = 3
- }
- private enum SettingsAction: Int {
- case restore = 0
- case rateUs = 1
- case support = 2
- case moreApps = 3
- case shareApp = 4
- }
- final class ViewController: NSViewController {
- private let palette = Palette()
- private let typography = Typography()
- private var mainContentHost: NSView?
- private var sidebarRowViews: [SidebarPage: NSView] = [:]
- private var tabViews: [MeetingProvider: NSView] = [:]
- private var selectedSidebarPage: SidebarPage = .joinMeetings
- private var selectedMeetingProvider: MeetingProvider = .meet
- private var pageCache: [SidebarPage: NSView] = [:]
- private var sidebarPageByView = [ObjectIdentifier: SidebarPage]()
- private var meetingProviderByView = [ObjectIdentifier: MeetingProvider]()
- private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
- private let darkModeDefaultsKey = "settings.darkModeEnabled"
- private var darkModeEnabled: Bool {
- get {
- let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
- return hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : true
- }
- set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
- }
- private lazy var settingsPopover: NSPopover = {
- let popover = NSPopover()
- popover.behavior = .transient
- popover.animates = true
- popover.contentViewController = SettingsMenuViewController(
- palette: palette,
- typography: typography,
- darkModeEnabled: darkModeEnabled,
- onToggleDarkMode: { [weak self] enabled in
- self?.setDarkMode(enabled)
- },
- onAction: { [weak self] action in
- self?.handleSettingsAction(action)
- }
- )
- return popover
- }()
- override func viewDidLoad() {
- super.viewDidLoad()
- setupRootView()
- buildMainLayout()
- }
- override func viewDidAppear() {
- super.viewDidAppear()
- view.window?.setContentSize(NSSize(width: 1120, height: 690))
- view.window?.minSize = NSSize(width: 940, height: 600)
- applyWindowTitle(for: selectedSidebarPage)
- }
- override var representedObject: Any? {
- didSet {}
- }
- }
- private extension ViewController {
- func setupRootView() {
- view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
- view.wantsLayer = true
- view.layer?.backgroundColor = palette.pageBackground.cgColor
- }
- func buildMainLayout() {
- let splitContainer = NSStackView()
- splitContainer.translatesAutoresizingMaskIntoConstraints = false
- splitContainer.orientation = .horizontal
- splitContainer.spacing = 0
- splitContainer.alignment = .top
- view.addSubview(splitContainer)
- NSLayoutConstraint.activate([
- splitContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
- splitContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
- splitContainer.topAnchor.constraint(equalTo: view.topAnchor),
- splitContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
- ])
- let sidebar = makeSidebar()
- let mainPanel = makeMainPanel()
- splitContainer.addArrangedSubview(sidebar)
- splitContainer.addArrangedSubview(mainPanel)
- }
- @objc private func sidebarItemClicked(_ sender: NSClickGestureRecognizer) {
- guard let view = sender.view,
- let page = sidebarPageByView[ObjectIdentifier(view)],
- page != selectedSidebarPage || page == .settings else { return }
- if page == .settings {
- showSettingsPopover()
- return
- }
- showSidebarPage(page)
- }
- @objc private func meetingTabClicked(_ sender: NSClickGestureRecognizer) {
- guard let view = sender.view,
- let provider = meetingProviderByView[ObjectIdentifier(view)],
- provider != selectedMeetingProvider else { return }
- selectedMeetingProvider = provider
- updateTabAppearance()
- }
- private func showSidebarPage(_ page: SidebarPage) {
- selectedSidebarPage = page
- updateSidebarAppearance()
- applyWindowTitle(for: page)
- guard let host = mainContentHost else { return }
- host.subviews.forEach { $0.removeFromSuperview() }
- let child = viewForPage(page)
- child.translatesAutoresizingMaskIntoConstraints = false
- host.addSubview(child)
- NSLayoutConstraint.activate([
- child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
- child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
- child.topAnchor.constraint(equalTo: host.topAnchor),
- child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
- ])
- }
- private func showSettingsPopover() {
- guard let anchor = sidebarRowViews[.settings] else { return }
- if settingsPopover.isShown {
- settingsPopover.performClose(nil)
- return
- }
- if let menu = settingsPopover.contentViewController as? SettingsMenuViewController {
- menu.setDarkModeEnabled(darkModeEnabled)
- }
- settingsPopover.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .maxX)
- }
- private func setDarkMode(_ enabled: Bool) {
- darkModeEnabled = enabled
- NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
- view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
- }
- private func handleSettingsAction(_ action: SettingsAction) {
- switch action {
- case .restore:
- showSimpleAlert(title: "Restore", message: "Restore action tapped.")
- case .rateUs:
- // Replace with your App Store URL when ready.
- showSimpleAlert(title: "Rate Us", message: "Rate Us tapped (add App Store URL).")
- case .support:
- showSimpleAlert(title: "Support", message: "Support tapped (add support email / page).")
- case .moreApps:
- showSimpleAlert(title: "More Apps", message: "More Apps tapped (add developer page URL).")
- case .shareApp:
- let urlString = "https://example.com"
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(urlString, forType: .string)
- showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
- }
- }
- private func showSimpleAlert(title: String, message: String) {
- let alert = NSAlert()
- alert.messageText = title
- alert.informativeText = message
- alert.addButton(withTitle: "OK")
- alert.runModal()
- }
- private func viewForPage(_ page: SidebarPage) -> NSView {
- if let cached = pageCache[page] { return cached }
- let built: NSView
- switch page {
- case .joinMeetings:
- built = makeJoinMeetingsContent()
- case .photo:
- built = makePlaceholderPage(title: "Photo", subtitle: "Backgrounds — choose a photo background for your meetings.")
- case .video:
- built = makePlaceholderPage(title: "Video", subtitle: "Backgrounds — video background options.")
- case .tutorials:
- built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.")
- case .settings:
- built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.")
- }
- pageCache[page] = built
- return built
- }
- private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
- let panel = NSView()
- panel.translatesAutoresizingMaskIntoConstraints = false
- let titleLabel = textLabel(title, font: typography.pageTitle, color: palette.textPrimary)
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- let sub = textLabel(subtitle, font: typography.fieldLabel, color: palette.textSecondary)
- sub.translatesAutoresizingMaskIntoConstraints = false
- panel.addSubview(titleLabel)
- panel.addSubview(sub)
- NSLayoutConstraint.activate([
- titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
- titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26),
- sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
- sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
- ])
- return panel
- }
- private func applyWindowTitle(for page: SidebarPage) {
- switch page {
- case .joinMeetings:
- view.window?.title = "App for Google Meet"
- case .photo:
- view.window?.title = "Backgrounds — Photo"
- case .video:
- view.window?.title = "Backgrounds — Video"
- case .tutorials:
- view.window?.title = "Tutorials"
- case .settings:
- view.window?.title = "Settings"
- }
- }
- private func updateSidebarAppearance() {
- for (page, row) in sidebarRowViews {
- applySidebarRowStyle(row, page: page, logoTemplate: logoTemplateForSidebarPage(page))
- }
- }
- private func updateTabAppearance() {
- for (provider, tab) in tabViews {
- applyTabStyle(tab, provider: provider, logoTemplate: logoTemplateForMeetingProvider(provider))
- }
- }
- private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
- switch page {
- case .photo, .tutorials: return false
- case .joinMeetings, .video, .settings: return true
- }
- }
- private func logoTemplateForMeetingProvider(_ provider: MeetingProvider) -> Bool {
- switch provider {
- case .teams: return false
- case .meet, .zoom, .zoho: return true
- }
- }
- func makeSidebar() -> NSView {
- let sidebar = NSView()
- sidebar.translatesAutoresizingMaskIntoConstraints = false
- sidebar.wantsLayer = true
- sidebar.layer?.backgroundColor = palette.sidebarBackground.cgColor
- sidebar.layer?.borderColor = palette.separator.cgColor
- sidebar.layer?.borderWidth = 1
- sidebar.layer?.shadowColor = NSColor.black.cgColor
- sidebar.layer?.shadowOpacity = 0.18
- sidebar.layer?.shadowOffset = CGSize(width: 2, height: 0)
- sidebar.layer?.shadowRadius = 10
- sidebar.widthAnchor.constraint(equalToConstant: 210).isActive = true
- let titleRow = NSStackView(views: [
- iconLabel("📅", size: 24),
- textLabel("Meetings", font: typography.sidebarBrand, color: palette.textPrimary)
- ])
- titleRow.translatesAutoresizingMaskIntoConstraints = false
- titleRow.orientation = .horizontal
- titleRow.alignment = .centerY
- titleRow.spacing = 8
- let menuStack = NSStackView()
- menuStack.translatesAutoresizingMaskIntoConstraints = false
- menuStack.orientation = .vertical
- menuStack.alignment = .leading
- menuStack.spacing = 10
- menuStack.addArrangedSubview(sidebarSectionTitle("Meetings"))
- let joinRow = sidebarItem("Join Meetings", icon: "", page: .joinMeetings, logoImageName: "JoinMeetingsLogo", logoIconWidth: 24, logoHeightMultiplier: 56.0 / 52.0)
- menuStack.addArrangedSubview(joinRow)
- sidebarRowViews[.joinMeetings] = joinRow
- menuStack.addArrangedSubview(sidebarSectionTitle("Backgrounds"))
- let photoRow = sidebarItem("Photo", icon: "", page: .photo, logoImageName: "SidebarPhotoLogo", logoIconWidth: 24, logoHeightMultiplier: 82.0 / 62.0, logoTemplate: false)
- menuStack.addArrangedSubview(photoRow)
- sidebarRowViews[.photo] = photoRow
- let videoRow = sidebarItem("Video", icon: "", page: .video, logoImageName: "SidebarVideoLogo", logoIconWidth: 28, logoHeightMultiplier: 52.0 / 60.0)
- menuStack.addArrangedSubview(videoRow)
- sidebarRowViews[.video] = videoRow
- menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
- let tutorialsRow = sidebarItem("Tutorials", icon: "", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0, logoTemplate: false)
- menuStack.addArrangedSubview(tutorialsRow)
- sidebarRowViews[.tutorials] = tutorialsRow
- let settingsRow = sidebarItem("Settings", icon: "", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true)
- menuStack.addArrangedSubview(settingsRow)
- sidebarRowViews[.settings] = settingsRow
- sidebar.addSubview(titleRow)
- sidebar.addSubview(menuStack)
- NSLayoutConstraint.activate([
- titleRow.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 16),
- titleRow.topAnchor.constraint(equalTo: sidebar.topAnchor, constant: 24),
- titleRow.trailingAnchor.constraint(lessThanOrEqualTo: sidebar.trailingAnchor, constant: -16),
- menuStack.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12),
- menuStack.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12),
- menuStack.topAnchor.constraint(equalTo: titleRow.bottomAnchor, constant: 20)
- ])
- for subview in menuStack.arrangedSubviews {
- subview.widthAnchor.constraint(equalTo: menuStack.widthAnchor).isActive = true
- }
- return sidebar
- }
- func makeMainPanel() -> NSView {
- let panel = NSView()
- panel.translatesAutoresizingMaskIntoConstraints = false
- panel.wantsLayer = true
- panel.layer?.backgroundColor = palette.pageBackground.cgColor
- let host = NSView()
- host.translatesAutoresizingMaskIntoConstraints = false
- panel.addSubview(host)
- NSLayoutConstraint.activate([
- host.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
- host.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
- host.topAnchor.constraint(equalTo: panel.topAnchor),
- host.bottomAnchor.constraint(equalTo: panel.bottomAnchor)
- ])
- mainContentHost = host
- showSidebarPage(.joinMeetings)
- return panel
- }
- func makeJoinMeetingsContent() -> NSView {
- let panel = NSView()
- panel.translatesAutoresizingMaskIntoConstraints = false
- let contentStack = NSStackView()
- contentStack.translatesAutoresizingMaskIntoConstraints = false
- contentStack.orientation = .vertical
- contentStack.spacing = 14
- contentStack.alignment = .leading
- contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
- contentStack.addArrangedSubview(meetingTypeTabs())
- contentStack.addArrangedSubview(joinWithURLHeading())
- contentStack.addArrangedSubview(meetingUrlSection())
- contentStack.addArrangedSubview(scheduleHeader())
- contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
- contentStack.addArrangedSubview(scheduleCardsRow())
- panel.addSubview(contentStack)
- NSLayoutConstraint.activate([
- contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
- contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
- contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26)
- ])
- return panel
- }
- func joinWithURLHeading() -> NSView {
- let container = NSView()
- container.translatesAutoresizingMaskIntoConstraints = false
- let title = textLabel("Join with URL", font: typography.joinWithURLTitle, color: palette.textPrimary)
- title.alignment = .left
- title.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- title.setContentCompressionResistancePriority(.required, for: .horizontal)
- let bar = NSView()
- bar.translatesAutoresizingMaskIntoConstraints = false
- bar.wantsLayer = true
- bar.layer?.backgroundColor = palette.primaryBlue.cgColor
- bar.heightAnchor.constraint(equalToConstant: 3).isActive = true
- container.addSubview(title)
- container.addSubview(bar)
- NSLayoutConstraint.activate([
- title.leadingAnchor.constraint(equalTo: container.leadingAnchor),
- title.topAnchor.constraint(equalTo: container.topAnchor),
- bar.leadingAnchor.constraint(equalTo: title.leadingAnchor),
- bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
- bar.widthAnchor.constraint(equalTo: title.widthAnchor),
- bar.bottomAnchor.constraint(equalTo: container.bottomAnchor),
- container.trailingAnchor.constraint(equalTo: title.trailingAnchor)
- ])
- return container
- }
- func meetingTypeTabs() -> NSView {
- let wrapper = NSView()
- wrapper.translatesAutoresizingMaskIntoConstraints = false
- let shell = roundedContainer(cornerRadius: 24, color: palette.tabBarBackground)
- shell.translatesAutoresizingMaskIntoConstraints = false
- shell.heightAnchor.constraint(equalToConstant: 48).isActive = true
- let stack = NSStackView()
- stack.translatesAutoresizingMaskIntoConstraints = false
- stack.orientation = .horizontal
- stack.distribution = .fillEqually
- stack.spacing = 4
- let meetTab = topTab("Meet", icon: "", provider: .meet, logoImageName: "MeetLogo")
- stack.addArrangedSubview(meetTab)
- tabViews[.meet] = meetTab
- let zoomTab = topTab("Zoom", icon: "", provider: .zoom, logoImageName: "ZoomLogo", logoPointSize: 34)
- stack.addArrangedSubview(zoomTab)
- tabViews[.zoom] = zoomTab
- let teamsTab = topTab("Teams", icon: "", provider: .teams, logoImageName: "TeamsLogo", logoPointSize: 26, logoHeightMultiplier: 62.0 / 50.0, logoTemplate: false)
- stack.addArrangedSubview(teamsTab)
- tabViews[.teams] = teamsTab
- let zohoTab = topTab("Zoho", icon: "", provider: .zoho, logoImageName: "ZohoLogo", logoPointSize: 28)
- stack.addArrangedSubview(zohoTab)
- tabViews[.zoho] = zohoTab
- shell.addSubview(stack)
- wrapper.addSubview(shell)
- NSLayoutConstraint.activate([
- wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
- shell.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
- shell.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
- shell.topAnchor.constraint(equalTo: wrapper.topAnchor),
- shell.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
- stack.leadingAnchor.constraint(equalTo: shell.leadingAnchor, constant: 72),
- stack.trailingAnchor.constraint(equalTo: shell.trailingAnchor, constant: -28),
- stack.topAnchor.constraint(equalTo: shell.topAnchor, constant: 6),
- stack.bottomAnchor.constraint(equalTo: shell.bottomAnchor, constant: -6)
- ])
- return wrapper
- }
- func meetingUrlSection() -> NSView {
- let wrapper = NSView()
- wrapper.translatesAutoresizingMaskIntoConstraints = false
- let title = textLabel("Meeting URL", font: typography.fieldLabel, color: palette.textSecondary)
- let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
- textFieldContainer.translatesAutoresizingMaskIntoConstraints = false
- textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true
- styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
- let urlField = NSTextField(string: "")
- urlField.translatesAutoresizingMaskIntoConstraints = false
- urlField.isEditable = true
- urlField.isSelectable = true
- urlField.isBordered = false
- urlField.drawsBackground = false
- urlField.placeholderString = "Enter meeting URL..."
- urlField.font = typography.inputPlaceholder
- urlField.textColor = palette.textPrimary
- urlField.focusRingType = .none
- textFieldContainer.addSubview(urlField)
- let actions = NSStackView()
- actions.orientation = .horizontal
- actions.spacing = 10
- actions.translatesAutoresizingMaskIntoConstraints = false
- actions.alignment = .centerY
- actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
- actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
- wrapper.addSubview(title)
- wrapper.addSubview(textFieldContainer)
- wrapper.addSubview(actions)
- NSLayoutConstraint.activate([
- wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
- title.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
- title.topAnchor.constraint(equalTo: wrapper.topAnchor),
- textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
- textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
- textFieldContainer.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 10),
- urlField.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12),
- urlField.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12),
- urlField.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor),
- actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
- actions.topAnchor.constraint(equalTo: textFieldContainer.bottomAnchor, constant: 14),
- actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
- ])
- return wrapper
- }
- func scheduleHeader() -> NSView {
- let row = NSStackView()
- row.translatesAutoresizingMaskIntoConstraints = false
- row.orientation = .horizontal
- row.alignment = .centerY
- row.distribution = .fill
- row.spacing = 12
- row.addArrangedSubview(textLabel("Schedule", font: typography.sectionTitleBold, color: palette.textPrimary))
- let spacer = NSView()
- spacer.translatesAutoresizingMaskIntoConstraints = false
- row.addArrangedSubview(spacer)
- spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
- row.addArrangedSubview(iconRoundButton("?", size: 34))
- row.addArrangedSubview(iconRoundButton("⟳", size: 34))
- let filter = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
- filter.translatesAutoresizingMaskIntoConstraints = false
- filter.widthAnchor.constraint(equalToConstant: 156).isActive = true
- filter.heightAnchor.constraint(equalToConstant: 34).isActive = true
- styleSurface(filter, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
- let filterText = textLabel("All", font: typography.filterText, color: palette.textSecondary)
- let arrow = textLabel("▾", font: typography.filterArrow, color: palette.textMuted)
- filterText.translatesAutoresizingMaskIntoConstraints = false
- arrow.translatesAutoresizingMaskIntoConstraints = false
- filter.addSubview(filterText)
- filter.addSubview(arrow)
- NSLayoutConstraint.activate([
- filterText.leadingAnchor.constraint(equalTo: filter.leadingAnchor, constant: 12),
- filterText.centerYAnchor.constraint(equalTo: filter.centerYAnchor),
- arrow.trailingAnchor.constraint(equalTo: filter.trailingAnchor, constant: -10),
- arrow.centerYAnchor.constraint(equalTo: filter.centerYAnchor)
- ])
- row.addArrangedSubview(filter)
- row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
- return row
- }
- func scheduleCardsRow() -> NSView {
- let row = NSStackView()
- row.translatesAutoresizingMaskIntoConstraints = false
- row.orientation = .horizontal
- row.spacing = 10
- row.alignment = .top
- row.distribution = .fill
- row.setContentHuggingPriority(.defaultHigh, for: .horizontal)
- row.heightAnchor.constraint(equalToConstant: 136).isActive = true
- row.addArrangedSubview(scheduleCard())
- row.addArrangedSubview(scheduleCard())
- return row
- }
- func scheduleCard() -> NSView {
- let cardWidth: CGFloat = 264
- let card = roundedContainer(cornerRadius: 10, 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: 136).isActive = true
- let icon = roundedContainer(cornerRadius: 5, color: palette.meetingBadge)
- icon.translatesAutoresizingMaskIntoConstraints = false
- icon.widthAnchor.constraint(equalToConstant: 22).isActive = true
- icon.heightAnchor.constraint(equalToConstant: 22).isActive = true
- let iconText = textLabel("••", font: typography.cardIcon, color: .white)
- iconText.translatesAutoresizingMaskIntoConstraints = false
- icon.addSubview(iconText)
- NSLayoutConstraint.activate([
- iconText.centerXAnchor.constraint(equalTo: icon.centerXAnchor),
- iconText.centerYAnchor.constraint(equalTo: icon.centerYAnchor)
- ])
- let title = textLabel("General Meeting", font: typography.cardTitle, color: palette.textPrimary)
- let subtitle = textLabel("Baisakhi", font: typography.cardSubtitle, color: palette.textPrimary)
- let time = textLabel("12:00 AM - 11:59 PM", font: typography.cardTime, color: palette.textSecondary)
- card.addSubview(icon)
- card.addSubview(title)
- card.addSubview(subtitle)
- card.addSubview(time)
- NSLayoutConstraint.activate([
- icon.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
- icon.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
- title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 6),
- title.centerYAnchor.constraint(equalTo: icon.centerYAnchor),
- title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -10),
- subtitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
- subtitle.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 7),
- time.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
- time.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 4)
- ])
- return card
- }
- }
- /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
- private class RowHitTestView: NSView {
- override func hitTest(_ point: NSPoint) -> NSView? {
- guard let superview else { return nil }
- let local = convert(point, from: superview)
- return bounds.contains(local) ? self : nil
- }
- }
- private final class HoverTrackingView: RowHitTestView {
- var onHoverChanged: ((Bool) -> Void)?
- var showsHandCursor = true
- private var trackingAreaRef: NSTrackingArea?
- private var isHovering = false {
- didSet {
- guard isHovering != oldValue else { return }
- onHoverChanged?(isHovering)
- }
- }
- override func updateTrackingAreas() {
- super.updateTrackingAreas()
- if let trackingAreaRef {
- removeTrackingArea(trackingAreaRef)
- }
- let options: NSTrackingArea.Options = [
- .activeInKeyWindow,
- .inVisibleRect,
- .mouseEnteredAndExited
- ]
- let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
- addTrackingArea(area)
- trackingAreaRef = area
- }
- override func mouseEntered(with event: NSEvent) {
- super.mouseEntered(with: event)
- isHovering = true
- }
- override func mouseExited(with event: NSEvent) {
- super.mouseExited(with: event)
- isHovering = false
- }
- override func resetCursorRects() {
- super.resetCursorRects()
- guard showsHandCursor else { return }
- addCursorRect(bounds, cursor: .pointingHand)
- }
- }
- private final class SettingsMenuViewController: NSViewController {
- private let palette: Palette
- private let typography: Typography
- private let onToggleDarkMode: (Bool) -> Void
- private let onAction: (SettingsAction) -> Void
- private var darkToggle: NSSwitch?
- init(
- palette: Palette,
- typography: Typography,
- darkModeEnabled: Bool,
- onToggleDarkMode: @escaping (Bool) -> Void,
- onAction: @escaping (SettingsAction) -> Void
- ) {
- self.palette = palette
- self.typography = typography
- self.onToggleDarkMode = onToggleDarkMode
- self.onAction = onAction
- super.init(nibName: nil, bundle: nil)
- self.view = makeView(darkModeEnabled: darkModeEnabled)
- }
- @available(*, unavailable)
- required init?(coder: NSCoder) {
- nil
- }
- func setDarkModeEnabled(_ enabled: Bool) {
- darkToggle?.state = enabled ? .on : .off
- }
- private func makeView(darkModeEnabled: Bool) -> NSView {
- let root = NSView()
- root.translatesAutoresizingMaskIntoConstraints = false
- let card = roundedCard()
- root.addSubview(card)
- NSLayoutConstraint.activate([
- card.leadingAnchor.constraint(equalTo: root.leadingAnchor),
- card.trailingAnchor.constraint(equalTo: root.trailingAnchor),
- card.topAnchor.constraint(equalTo: root.topAnchor),
- card.bottomAnchor.constraint(equalTo: root.bottomAnchor),
- root.widthAnchor.constraint(equalToConstant: 260)
- ])
- let stack = NSStackView()
- stack.translatesAutoresizingMaskIntoConstraints = false
- stack.orientation = .vertical
- stack.spacing = 6
- stack.alignment = .leading
- card.addSubview(stack)
- NSLayoutConstraint.activate([
- stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
- stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
- stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
- stack.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -14)
- ])
- stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled))
- stack.addArrangedSubview(settingsActionRow(icon: "⟳", title: "Restore", action: .restore))
- stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
- stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
- stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
- stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
- for v in stack.arrangedSubviews {
- v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
- }
- return root
- }
- private func roundedCard() -> NSView {
- let view = NSView()
- view.translatesAutoresizingMaskIntoConstraints = false
- view.wantsLayer = true
- view.layer?.cornerRadius = 12
- view.layer?.backgroundColor = NSColor(calibratedWhite: 0.12, alpha: 1).cgColor
- view.layer?.borderColor = NSColor(calibratedWhite: 0.22, alpha: 1).cgColor
- view.layer?.borderWidth = 1
- view.layer?.shadowColor = NSColor.black.cgColor
- view.layer?.shadowOpacity = 0.28
- view.layer?.shadowOffset = CGSize(width: 0, height: -1)
- view.layer?.shadowRadius = 10
- return view
- }
- private func settingsDarkModeRow(enabled: Bool) -> NSView {
- let row = HoverTrackingView()
- row.translatesAutoresizingMaskIntoConstraints = false
- row.heightAnchor.constraint(equalToConstant: 44).isActive = true
- let icon = NSTextField(labelWithString: "◐")
- icon.translatesAutoresizingMaskIntoConstraints = false
- icon.font = NSFont.systemFont(ofSize: 18, weight: .medium)
- icon.textColor = .white
- let title = NSTextField(labelWithString: "Dark Mode")
- title.translatesAutoresizingMaskIntoConstraints = false
- title.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
- title.textColor = .white
- let toggle = NSSwitch()
- toggle.translatesAutoresizingMaskIntoConstraints = false
- toggle.state = enabled ? .on : .off
- toggle.target = self
- toggle.action = #selector(darkModeToggled(_:))
- darkToggle = toggle
- row.addSubview(icon)
- row.addSubview(title)
- row.addSubview(toggle)
- row.onHoverChanged = { hovering in
- row.wantsLayer = true
- row.layer?.cornerRadius = 10
- row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
- }
- row.onHoverChanged?(false)
- NSLayoutConstraint.activate([
- icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
- icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
- title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10),
- title.centerYAnchor.constraint(equalTo: row.centerYAnchor),
- toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -2),
- toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor)
- ])
- return row
- }
- private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
- let row = HoverTrackingView()
- row.translatesAutoresizingMaskIntoConstraints = false
- row.heightAnchor.constraint(equalToConstant: 42).isActive = true
- let iconLabel = NSTextField(labelWithString: icon)
- iconLabel.translatesAutoresizingMaskIntoConstraints = false
- iconLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
- iconLabel.textColor = .white
- let titleLabel = NSTextField(labelWithString: title)
- titleLabel.translatesAutoresizingMaskIntoConstraints = false
- titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
- titleLabel.textColor = .white
- row.addSubview(iconLabel)
- row.addSubview(titleLabel)
- NSLayoutConstraint.activate([
- iconLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
- iconLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
- titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10),
- titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor)
- ])
- let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
- row.addGestureRecognizer(click)
- row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
- row.onHoverChanged = { hovering in
- row.wantsLayer = true
- row.layer?.cornerRadius = 10
- row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
- }
- row.onHoverChanged?(false)
- return row
- }
- @objc private func darkModeToggled(_ sender: NSSwitch) {
- onToggleDarkMode(sender.state == .on)
- }
- @objc private func settingsActionClicked(_ sender: NSClickGestureRecognizer) {
- guard let view = sender.view,
- let raw = Int(view.identifier?.rawValue ?? ""),
- let action = SettingsAction(rawValue: raw) else { return }
- onAction(action)
- }
- }
- private extension ViewController {
- func roundedContainer(cornerRadius: CGFloat, color: NSColor) -> NSView {
- let view = NSView()
- view.wantsLayer = true
- view.layer?.backgroundColor = color.cgColor
- view.layer?.cornerRadius = cornerRadius
- return view
- }
- func styleSurface(_ view: NSView, borderColor: NSColor, borderWidth: CGFloat, shadow: Bool) {
- view.layer?.borderColor = borderColor.cgColor
- view.layer?.borderWidth = borderWidth
- if shadow {
- view.layer?.shadowColor = NSColor.black.cgColor
- view.layer?.shadowOpacity = 0.18
- view.layer?.shadowOffset = CGSize(width: 0, height: -1)
- view.layer?.shadowRadius = 5
- }
- }
- func textLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField {
- let label = NSTextField(labelWithString: text)
- label.translatesAutoresizingMaskIntoConstraints = false
- label.textColor = color
- label.font = font
- return label
- }
- func iconLabel(_ text: String, size: CGFloat) -> NSTextField {
- let label = NSTextField(labelWithString: text)
- label.translatesAutoresizingMaskIntoConstraints = false
- label.font = NSFont.systemFont(ofSize: size)
- return label
- }
- func sidebarSectionTitle(_ text: String) -> NSTextField {
- let field = textLabel(text, font: typography.sidebarSection, color: palette.textMuted)
- field.alignment = .left
- return field
- }
- func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
- let item = HoverTrackingView()
- item.wantsLayer = true
- item.layer?.cornerRadius = 10
- item.layer?.backgroundColor = NSColor.clear.cgColor
- item.translatesAutoresizingMaskIntoConstraints = false
- item.heightAnchor.constraint(equalToConstant: 36).isActive = true
- item.layer?.borderWidth = 0
- sidebarPageByView[ObjectIdentifier(item)] = page
- let leadingView: NSView
- if let name = logoImageName, let logo = NSImage(named: name) {
- let imageView = NSImageView(image: logo)
- imageView.translatesAutoresizingMaskIntoConstraints = false
- imageView.imageScaling = .scaleProportionallyDown
- imageView.imageAlignment = .alignCenter
- imageView.isEditable = false
- leadingView = imageView
- } else {
- leadingView = textLabel(icon, font: typography.sidebarIcon, color: palette.textSecondary)
- }
- let titleLabel = textLabel(text, font: typography.sidebarItem, color: palette.textSecondary)
- titleLabel.alignment = .left
- item.addSubview(leadingView)
- item.addSubview(titleLabel)
- var constraints: [NSLayoutConstraint] = [
- leadingView.leadingAnchor.constraint(equalTo: item.leadingAnchor, constant: 12),
- leadingView.centerYAnchor.constraint(equalTo: item.centerYAnchor),
- titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 8),
- titleLabel.centerYAnchor.constraint(equalTo: item.centerYAnchor)
- ]
- if showsDisclosure {
- let chevron = textLabel("›", font: NSFont.systemFont(ofSize: 22, weight: .semibold), color: palette.textSecondary)
- chevron.translatesAutoresizingMaskIntoConstraints = false
- chevron.alignment = .right
- item.addSubview(chevron)
- constraints.append(contentsOf: [
- chevron.trailingAnchor.constraint(equalTo: item.trailingAnchor, constant: -10),
- chevron.centerYAnchor.constraint(equalTo: item.centerYAnchor),
- titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8)
- ])
- } else {
- constraints.append(titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: item.trailingAnchor, constant: -10))
- }
- if logoImageName != nil {
- let h = logoIconWidth * logoHeightMultiplier
- constraints.append(contentsOf: [
- leadingView.widthAnchor.constraint(equalToConstant: logoIconWidth),
- leadingView.heightAnchor.constraint(equalToConstant: h)
- ])
- }
- NSLayoutConstraint.activate(constraints)
- applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
- item.onHoverChanged = { [weak self, weak item] hovering in
- guard let self, let item else { return }
- self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
- }
- let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
- item.addGestureRecognizer(click)
- return item
- }
- func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
- let selected = (page == selectedSidebarPage)
- let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
- item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
- let tint = selected ? NSColor.white : palette.textSecondary
- guard item.subviews.count >= 2 else { return }
- let leading = item.subviews[0]
- let title = item.subviews.first { $0 is NSTextField } as? NSTextField
- title?.textColor = tint
- // Optional disclosure chevron (if present) is the last text field.
- if let chevron = item.subviews.last as? NSTextField, chevron !== title {
- chevron.textColor = tint
- }
- if let imageView = leading as? NSImageView {
- if logoTemplate {
- imageView.contentTintColor = tint
- }
- } else if let iconField = leading as? NSTextField {
- iconField.textColor = tint
- }
- }
- func topTab(_ title: String, icon: String, provider: MeetingProvider, logoImageName: String? = nil, logoPointSize: CGFloat = 26, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true) -> NSView {
- let tab = HoverTrackingView()
- tab.wantsLayer = true
- tab.layer?.cornerRadius = 19
- tab.layer?.backgroundColor = NSColor.clear.cgColor
- tab.translatesAutoresizingMaskIntoConstraints = false
- meetingProviderByView[ObjectIdentifier(tab)] = provider
- let leadingView: NSView
- if let name = logoImageName, let logo = NSImage(named: name) {
- let imageView = NSImageView(image: logo)
- imageView.translatesAutoresizingMaskIntoConstraints = false
- imageView.imageScaling = .scaleProportionallyDown
- imageView.imageAlignment = .alignCenter
- imageView.isEditable = false
- if logoTemplate {
- imageView.contentTintColor = palette.textPrimary
- }
- leadingView = imageView
- } else {
- leadingView = textLabel(icon, font: typography.tabIcon, color: palette.textPrimary)
- }
- let titleLabel = textLabel(title, font: typography.tabTitle, color: palette.textPrimary)
- tab.addSubview(leadingView)
- tab.addSubview(titleLabel)
- var constraints: [NSLayoutConstraint] = [
- leadingView.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 16),
- leadingView.centerYAnchor.constraint(equalTo: tab.centerYAnchor),
- titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 6),
- titleLabel.centerYAnchor.constraint(equalTo: tab.centerYAnchor)
- ]
- if logoImageName != nil {
- constraints.append(contentsOf: [
- leadingView.widthAnchor.constraint(equalToConstant: logoPointSize),
- leadingView.heightAnchor.constraint(equalToConstant: logoPointSize * logoHeightMultiplier)
- ])
- }
- NSLayoutConstraint.activate(constraints)
- applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate)
- tab.onHoverChanged = { [weak self, weak tab] hovering in
- guard let self, let tab else { return }
- self.applyTabStyle(tab, provider: provider, logoTemplate: logoTemplate, hovering: hovering)
- }
- let click = NSClickGestureRecognizer(target: self, action: #selector(meetingTabClicked(_:)))
- tab.addGestureRecognizer(click)
- return tab
- }
- func applyTabStyle(_ tab: NSView, provider: MeetingProvider, logoTemplate: Bool, hovering: Bool = false) {
- let selected = (provider == selectedMeetingProvider)
- let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
- tab.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
- guard tab.subviews.count >= 2 else { return }
- let leading = tab.subviews[0]
- let title = tab.subviews[1] as? NSTextField
- let textColor = palette.textPrimary
- title?.textColor = textColor
- if let imageView = leading as? NSImageView {
- if logoTemplate {
- imageView.contentTintColor = textColor
- }
- } else if let iconField = leading as? NSTextField {
- iconField.textColor = textColor
- }
- }
- func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
- let button = HoverTrackingView()
- button.wantsLayer = true
- button.layer?.cornerRadius = 9
- button.layer?.backgroundColor = color.cgColor
- button.translatesAutoresizingMaskIntoConstraints = false
- button.widthAnchor.constraint(equalToConstant: width).isActive = true
- button.heightAnchor.constraint(equalToConstant: 36).isActive = true
- styleSurface(button, borderColor: title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder, borderWidth: 1, shadow: false)
- if title == "Cancel" {
- button.layer?.backgroundColor = palette.cancelButton.cgColor
- }
- let label = textLabel(title, font: typography.buttonText, color: textColor)
- button.addSubview(label)
- NSLayoutConstraint.activate([
- label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
- label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
- ])
- let baseColor = (title == "Cancel") ? palette.cancelButton : color
- let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
- button.onHoverChanged = { hovering in
- button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
- }
- button.onHoverChanged?(false)
- return button
- }
- func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
- let button = HoverTrackingView()
- button.wantsLayer = true
- button.layer?.cornerRadius = size / 2
- button.layer?.backgroundColor = palette.inputBackground.cgColor
- button.translatesAutoresizingMaskIntoConstraints = false
- button.widthAnchor.constraint(equalToConstant: size).isActive = true
- button.heightAnchor.constraint(equalToConstant: size).isActive = true
- styleSurface(button, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
- let label = textLabel(symbol, font: typography.iconButton, color: palette.textSecondary)
- button.addSubview(label)
- NSLayoutConstraint.activate([
- label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
- label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
- ])
- let baseColor = palette.inputBackground
- let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
- button.onHoverChanged = { hovering in
- button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
- }
- button.onHoverChanged?(false)
- return button
- }
- }
- private struct Palette {
- let pageBackground = NSColor(calibratedRed: 10.0 / 255.0, green: 11.0 / 255.0, blue: 12.0 / 255.0, alpha: 1)
- let sidebarBackground = NSColor(calibratedRed: 16.0 / 255.0, green: 17.0 / 255.0, blue: 19.0 / 255.0, alpha: 1)
- let sectionCard = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
- let tabBarBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
- let tabIdleBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
- let inputBackground = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
- let inputBorder = NSColor(calibratedRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1)
- let primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1)
- let primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1)
- let cancelButton = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
- let meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1)
- let separator = NSColor(calibratedRed: 26.0 / 255.0, green: 27.0 / 255.0, blue: 30.0 / 255.0, alpha: 1)
- let textPrimary = NSColor(calibratedWhite: 0.98, alpha: 1)
- let textSecondary = NSColor(calibratedWhite: 0.78, alpha: 1)
- let textTertiary = NSColor(calibratedWhite: 0.66, alpha: 1)
- let textMuted = NSColor(calibratedWhite: 0.44, alpha: 1)
- }
- private struct Typography {
- let sidebarBrand = NSFont.systemFont(ofSize: 26, weight: .bold)
- let sidebarSection = NSFont.systemFont(ofSize: 11, weight: .medium)
- let sidebarIcon = NSFont.systemFont(ofSize: 12, weight: .medium)
- let sidebarItem = NSFont.systemFont(ofSize: 16, weight: .medium)
- let pageTitle = NSFont.systemFont(ofSize: 27, weight: .semibold)
- let joinWithURLTitle = NSFont.systemFont(ofSize: 17, weight: .semibold)
- let sectionTitleBold = NSFont.systemFont(ofSize: 25, weight: .bold)
- let dateHeading = NSFont.systemFont(ofSize: 18, weight: .medium)
- let tabIcon = NSFont.systemFont(ofSize: 13, weight: .regular)
- let tabTitle = NSFont.systemFont(ofSize: 31 / 2, weight: .semibold)
- let fieldLabel = NSFont.systemFont(ofSize: 15, weight: .medium)
- let inputPlaceholder = NSFont.systemFont(ofSize: 14, weight: .regular)
- let buttonText = NSFont.systemFont(ofSize: 16, weight: .medium)
- let filterText = NSFont.systemFont(ofSize: 15, weight: .regular)
- let filterArrow = NSFont.systemFont(ofSize: 12, weight: .regular)
- let iconButton = NSFont.systemFont(ofSize: 14, weight: .medium)
- let cardIcon = NSFont.systemFont(ofSize: 8, weight: .bold)
- let cardTitle = NSFont.systemFont(ofSize: 15, weight: .semibold)
- let cardSubtitle = NSFont.systemFont(ofSize: 13, weight: .bold)
- let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
- }
|