Keine Beschreibung

ViewController.swift 85KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919
  1. //
  2. // ViewController.swift
  3. // meetings_app
  4. //
  5. // Created by Dev Mac 1 on 06/04/2026.
  6. //
  7. import Cocoa
  8. private enum SidebarPage: Int {
  9. case joinMeetings = 0
  10. case photo = 1
  11. case video = 2
  12. case tutorials = 3
  13. case settings = 4
  14. }
  15. private enum ZoomJoinMode: Int {
  16. case id = 0
  17. case url = 1
  18. }
  19. private enum SettingsAction: Int {
  20. case restore = 0
  21. case rateUs = 1
  22. case support = 2
  23. case moreApps = 3
  24. case shareApp = 4
  25. }
  26. private enum PremiumPlan: Int {
  27. case weekly = 0
  28. case monthly = 1
  29. case yearly = 2
  30. case lifetime = 3
  31. }
  32. final class ViewController: NSViewController {
  33. private let palette = Palette()
  34. private let typography = Typography()
  35. private let launchContentSize = NSSize(width: 920, height: 690)
  36. private let launchMinContentSize = NSSize(width: 760, height: 600)
  37. private var mainContentHost: NSView?
  38. private var sidebarRowViews: [SidebarPage: NSView] = [:]
  39. private var selectedSidebarPage: SidebarPage = .joinMeetings
  40. private var selectedZoomJoinMode: ZoomJoinMode = .id
  41. private var pageCache: [SidebarPage: NSView] = [:]
  42. private var sidebarPageByView = [ObjectIdentifier: SidebarPage]()
  43. private var zoomJoinModeByView = [ObjectIdentifier: ZoomJoinMode]()
  44. private var zoomJoinModeViews: [ZoomJoinMode: NSView] = [:]
  45. private var settingsActionByView = [ObjectIdentifier: SettingsAction]()
  46. private weak var centeredTitleLabel: NSTextField?
  47. private weak var paywallWindow: NSWindow?
  48. private let paywallContentWidth: CGFloat = 520
  49. private var selectedPremiumPlan: PremiumPlan = .monthly
  50. private var paywallPlanViews: [PremiumPlan: NSView] = [:]
  51. private var premiumPlanByView = [ObjectIdentifier: PremiumPlan]()
  52. private let darkModeDefaultsKey = "settings.darkModeEnabled"
  53. private var darkModeEnabled: Bool {
  54. get {
  55. let hasValue = UserDefaults.standard.object(forKey: darkModeDefaultsKey) != nil
  56. return hasValue ? UserDefaults.standard.bool(forKey: darkModeDefaultsKey) : true
  57. }
  58. set { UserDefaults.standard.set(newValue, forKey: darkModeDefaultsKey) }
  59. }
  60. private lazy var settingsPopover: NSPopover = {
  61. let popover = NSPopover()
  62. popover.behavior = .transient
  63. popover.animates = true
  64. popover.contentViewController = SettingsMenuViewController(
  65. palette: palette,
  66. typography: typography,
  67. darkModeEnabled: darkModeEnabled,
  68. onToggleDarkMode: { [weak self] enabled in
  69. self?.setDarkMode(enabled)
  70. },
  71. onAction: { [weak self] action in
  72. self?.handleSettingsAction(action)
  73. }
  74. )
  75. return popover
  76. }()
  77. override func viewDidLoad() {
  78. super.viewDidLoad()
  79. setupRootView()
  80. buildMainLayout()
  81. }
  82. override func viewDidAppear() {
  83. super.viewDidAppear()
  84. applyWindowTitle(for: selectedSidebarPage)
  85. guard let window = view.window else { return }
  86. // Ensure launch size is applied even when macOS tries to restore prior window state.
  87. window.isRestorable = false
  88. window.setFrameAutosaveName("")
  89. DispatchQueue.main.async { [weak self, weak window] in
  90. guard let self, let window else { return }
  91. let frameSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: self.launchContentSize)).size
  92. var newFrame = window.frame
  93. newFrame.size = frameSize
  94. window.setFrame(newFrame, display: true)
  95. window.center()
  96. window.minSize = window.frameRect(forContentRect: NSRect(origin: .zero, size: self.launchMinContentSize)).size
  97. self.installCenteredTitleIfNeeded(on: window)
  98. }
  99. }
  100. override var representedObject: Any? {
  101. didSet {}
  102. }
  103. }
  104. private extension ViewController {
  105. func setupRootView() {
  106. view.appearance = NSAppearance(named: darkModeEnabled ? .darkAqua : .aqua)
  107. view.wantsLayer = true
  108. view.layer?.backgroundColor = palette.pageBackground.cgColor
  109. }
  110. func buildMainLayout() {
  111. let splitContainer = NSStackView()
  112. splitContainer.translatesAutoresizingMaskIntoConstraints = false
  113. splitContainer.orientation = .horizontal
  114. splitContainer.spacing = 0
  115. splitContainer.alignment = .top
  116. view.addSubview(splitContainer)
  117. NSLayoutConstraint.activate([
  118. splitContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor),
  119. splitContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor),
  120. splitContainer.topAnchor.constraint(equalTo: view.topAnchor),
  121. splitContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor)
  122. ])
  123. let sidebar = makeSidebar()
  124. let mainPanel = makeMainPanel()
  125. splitContainer.addArrangedSubview(sidebar)
  126. splitContainer.addArrangedSubview(mainPanel)
  127. }
  128. @objc private func sidebarItemClicked(_ sender: NSClickGestureRecognizer) {
  129. guard let view = sender.view,
  130. let page = sidebarPageByView[ObjectIdentifier(view)],
  131. page != selectedSidebarPage || page == .settings else { return }
  132. if page == .settings {
  133. showSettingsPopover()
  134. return
  135. }
  136. showSidebarPage(page)
  137. }
  138. @objc private func zoomJoinModeClicked(_ sender: NSClickGestureRecognizer) {
  139. guard let view = sender.view,
  140. let mode = zoomJoinModeByView[ObjectIdentifier(view)],
  141. mode != selectedZoomJoinMode else { return }
  142. selectedZoomJoinMode = mode
  143. updateZoomJoinModeAppearance()
  144. if selectedSidebarPage == .joinMeetings {
  145. pageCache[.joinMeetings] = nil
  146. showSidebarPage(.joinMeetings)
  147. }
  148. }
  149. @objc private func premiumButtonClicked(_ sender: NSClickGestureRecognizer) {
  150. showPaywall()
  151. }
  152. private func showSidebarPage(_ page: SidebarPage) {
  153. selectedSidebarPage = page
  154. updateSidebarAppearance()
  155. applyWindowTitle(for: page)
  156. guard let host = mainContentHost else { return }
  157. host.subviews.forEach { $0.removeFromSuperview() }
  158. let child = viewForPage(page)
  159. child.translatesAutoresizingMaskIntoConstraints = false
  160. host.addSubview(child)
  161. NSLayoutConstraint.activate([
  162. child.leadingAnchor.constraint(equalTo: host.leadingAnchor),
  163. child.trailingAnchor.constraint(equalTo: host.trailingAnchor),
  164. child.topAnchor.constraint(equalTo: host.topAnchor),
  165. child.bottomAnchor.constraint(equalTo: host.bottomAnchor)
  166. ])
  167. }
  168. private func showSettingsPopover() {
  169. guard let anchor = sidebarRowViews[.settings] else { return }
  170. if settingsPopover.isShown {
  171. settingsPopover.performClose(nil)
  172. return
  173. }
  174. if let menu = settingsPopover.contentViewController as? SettingsMenuViewController {
  175. menu.setDarkModeEnabled(darkModeEnabled)
  176. }
  177. settingsPopover.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .maxX)
  178. }
  179. private func setDarkMode(_ enabled: Bool) {
  180. darkModeEnabled = enabled
  181. NSApp.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
  182. view.appearance = NSAppearance(named: enabled ? .darkAqua : .aqua)
  183. }
  184. private func handleSettingsAction(_ action: SettingsAction) {
  185. switch action {
  186. case .restore:
  187. showSimpleAlert(title: "Restore", message: "Restore action tapped.")
  188. case .rateUs:
  189. // Replace with your App Store URL when ready.
  190. showSimpleAlert(title: "Rate Us", message: "Rate Us tapped (add App Store URL).")
  191. case .support:
  192. showSimpleAlert(title: "Support", message: "Support tapped (add support email / page).")
  193. case .moreApps:
  194. showSimpleAlert(title: "More Apps", message: "More Apps tapped (add developer page URL).")
  195. case .shareApp:
  196. let urlString = "https://example.com"
  197. NSPasteboard.general.clearContents()
  198. NSPasteboard.general.setString(urlString, forType: .string)
  199. showSimpleAlert(title: "Share App", message: "Link copied to clipboard:\n\(urlString)")
  200. }
  201. }
  202. private func showSimpleAlert(title: String, message: String) {
  203. let alert = NSAlert()
  204. alert.messageText = title
  205. alert.informativeText = message
  206. alert.addButton(withTitle: "OK")
  207. alert.runModal()
  208. }
  209. private func showPaywall() {
  210. if let existing = paywallWindow {
  211. existing.makeKeyAndOrderFront(nil)
  212. NSApp.activate(ignoringOtherApps: true)
  213. return
  214. }
  215. let content = makePaywallContent()
  216. let controller = NSViewController()
  217. controller.view = content
  218. let panel = NSPanel(
  219. contentRect: NSRect(x: 0, y: 0, width: 640, height: 820),
  220. styleMask: [.titled, .closable, .fullSizeContentView],
  221. backing: .buffered,
  222. defer: false
  223. )
  224. panel.title = "Get Premium"
  225. panel.titleVisibility = .hidden
  226. panel.titlebarAppearsTransparent = true
  227. panel.isFloatingPanel = true
  228. panel.hidesOnDeactivate = false
  229. panel.standardWindowButton(.closeButton)?.isHidden = true
  230. panel.standardWindowButton(.miniaturizeButton)?.isHidden = true
  231. panel.standardWindowButton(.zoomButton)?.isHidden = true
  232. panel.center()
  233. panel.contentViewController = controller
  234. panel.makeKeyAndOrderFront(nil)
  235. NSApp.activate(ignoringOtherApps: true)
  236. paywallWindow = panel
  237. }
  238. @objc private func closePaywallClicked(_ sender: NSClickGestureRecognizer) {
  239. paywallWindow?.close()
  240. paywallWindow = nil
  241. }
  242. @objc private func paywallFooterLinkClicked(_ sender: NSClickGestureRecognizer) {
  243. guard let view = sender.view else { return }
  244. let text = (view.subviews.first { $0 is NSTextField } as? NSTextField)?.stringValue ?? "Link"
  245. showSimpleAlert(title: text, message: "\(text) tapped.")
  246. }
  247. @objc private func paywallPlanClicked(_ sender: NSClickGestureRecognizer) {
  248. guard let view = sender.view,
  249. let plan = premiumPlanByView[ObjectIdentifier(view)] else { return }
  250. selectedPremiumPlan = plan
  251. updatePaywallPlanSelection()
  252. }
  253. private func updatePaywallPlanSelection() {
  254. for (plan, view) in paywallPlanViews {
  255. applyPaywallPlanStyle(view, isSelected: plan == selectedPremiumPlan)
  256. }
  257. }
  258. private func applyPaywallPlanStyle(_ card: NSView, isSelected: Bool) {
  259. let selectedBorder = NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1)
  260. let idleBorder = palette.inputBorder
  261. let selectedBackground = NSColor(calibratedRed: 30.0 / 255.0, green: 34.0 / 255.0, blue: 42.0 / 255.0, alpha: 1)
  262. card.layer?.backgroundColor = (isSelected ? selectedBackground : palette.sectionCard).cgColor
  263. card.layer?.borderColor = (isSelected ? selectedBorder : idleBorder).cgColor
  264. card.layer?.borderWidth = isSelected ? 2 : 1
  265. card.layer?.shadowColor = NSColor.black.cgColor
  266. card.layer?.shadowOpacity = isSelected ? 0.26 : 0.12
  267. card.layer?.shadowOffset = CGSize(width: 0, height: -1)
  268. card.layer?.shadowRadius = isSelected ? 10 : 5
  269. }
  270. private func viewForPage(_ page: SidebarPage) -> NSView {
  271. if let cached = pageCache[page] { return cached }
  272. let built: NSView
  273. switch page {
  274. case .joinMeetings:
  275. built = makeJoinMeetingsContent()
  276. case .photo:
  277. built = makePlaceholderPage(title: "Photo", subtitle: "Backgrounds — choose a photo background for your meetings.")
  278. case .video:
  279. built = makePlaceholderPage(title: "Video", subtitle: "Backgrounds — video background options.")
  280. case .tutorials:
  281. built = makePlaceholderPage(title: "Tutorials", subtitle: "Learn how to use the app.")
  282. case .settings:
  283. built = makePlaceholderPage(title: "Settings", subtitle: "Preferences and account options.")
  284. }
  285. pageCache[page] = built
  286. return built
  287. }
  288. private func makePlaceholderPage(title: String, subtitle: String) -> NSView {
  289. let panel = NSView()
  290. panel.translatesAutoresizingMaskIntoConstraints = false
  291. let titleLabel = textLabel(title, font: typography.pageTitle, color: palette.textPrimary)
  292. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  293. let sub = textLabel(subtitle, font: typography.fieldLabel, color: palette.textSecondary)
  294. sub.translatesAutoresizingMaskIntoConstraints = false
  295. panel.addSubview(titleLabel)
  296. panel.addSubview(sub)
  297. NSLayoutConstraint.activate([
  298. titleLabel.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  299. titleLabel.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26),
  300. sub.leadingAnchor.constraint(equalTo: titleLabel.leadingAnchor),
  301. sub.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 8)
  302. ])
  303. return panel
  304. }
  305. private func applyWindowTitle(for page: SidebarPage) {
  306. let title: String
  307. switch page {
  308. case .joinMeetings:
  309. title = "App for Google Meet"
  310. case .photo:
  311. title = "Backgrounds — Photo"
  312. case .video:
  313. title = "Backgrounds — Video"
  314. case .tutorials:
  315. title = "Tutorials"
  316. case .settings:
  317. title = "Settings"
  318. }
  319. view.window?.title = title
  320. centeredTitleLabel?.stringValue = title
  321. }
  322. private func installCenteredTitleIfNeeded(on window: NSWindow) {
  323. guard centeredTitleLabel == nil else { return }
  324. guard let titlebarView = window.standardWindowButton(.closeButton)?.superview else { return }
  325. let label = NSTextField(labelWithString: window.title)
  326. label.translatesAutoresizingMaskIntoConstraints = false
  327. label.alignment = .center
  328. label.font = NSFont.titleBarFont(ofSize: 0)
  329. label.textColor = .labelColor
  330. label.lineBreakMode = .byTruncatingTail
  331. label.maximumNumberOfLines = 1
  332. titlebarView.addSubview(label)
  333. NSLayoutConstraint.activate([
  334. label.centerXAnchor.constraint(equalTo: titlebarView.centerXAnchor),
  335. label.centerYAnchor.constraint(equalTo: titlebarView.centerYAnchor),
  336. label.leadingAnchor.constraint(greaterThanOrEqualTo: titlebarView.leadingAnchor, constant: 90),
  337. label.trailingAnchor.constraint(lessThanOrEqualTo: titlebarView.trailingAnchor, constant: -90)
  338. ])
  339. window.titleVisibility = .hidden
  340. centeredTitleLabel = label
  341. }
  342. private func updateSidebarAppearance() {
  343. for (page, row) in sidebarRowViews {
  344. applySidebarRowStyle(row, page: page, logoTemplate: logoTemplateForSidebarPage(page))
  345. }
  346. }
  347. private func logoTemplateForSidebarPage(_ page: SidebarPage) -> Bool {
  348. switch page {
  349. case .photo, .tutorials: return false
  350. case .joinMeetings, .video, .settings: return true
  351. }
  352. }
  353. func makeSidebar() -> NSView {
  354. let sidebar = NSView()
  355. sidebar.translatesAutoresizingMaskIntoConstraints = false
  356. sidebar.wantsLayer = true
  357. sidebar.layer?.backgroundColor = palette.sidebarBackground.cgColor
  358. sidebar.layer?.borderColor = palette.separator.cgColor
  359. sidebar.layer?.borderWidth = 1
  360. sidebar.layer?.shadowColor = NSColor.black.cgColor
  361. sidebar.layer?.shadowOpacity = 0.18
  362. sidebar.layer?.shadowOffset = CGSize(width: 2, height: 0)
  363. sidebar.layer?.shadowRadius = 10
  364. sidebar.widthAnchor.constraint(equalToConstant: 210).isActive = true
  365. let titleRow = NSStackView(views: [
  366. iconLabel("📅", size: 24),
  367. textLabel("Meetings", font: typography.sidebarBrand, color: palette.textPrimary)
  368. ])
  369. titleRow.translatesAutoresizingMaskIntoConstraints = false
  370. titleRow.orientation = .horizontal
  371. titleRow.alignment = .centerY
  372. titleRow.spacing = 8
  373. let menuStack = NSStackView()
  374. menuStack.translatesAutoresizingMaskIntoConstraints = false
  375. menuStack.orientation = .vertical
  376. menuStack.alignment = .leading
  377. menuStack.spacing = 10
  378. menuStack.addArrangedSubview(sidebarSectionTitle("Meetings"))
  379. let joinRow = sidebarItem("Join Meetings", icon: "􀉣", page: .joinMeetings, logoImageName: "JoinMeetingsLogo", logoIconWidth: 24, logoHeightMultiplier: 56.0 / 52.0)
  380. menuStack.addArrangedSubview(joinRow)
  381. sidebarRowViews[.joinMeetings] = joinRow
  382. menuStack.addArrangedSubview(sidebarSectionTitle("Backgrounds"))
  383. let photoRow = sidebarItem("Photo", icon: "􀏂", page: .photo, logoImageName: "SidebarPhotoLogo", logoIconWidth: 24, logoHeightMultiplier: 82.0 / 62.0, logoTemplate: false)
  384. menuStack.addArrangedSubview(photoRow)
  385. sidebarRowViews[.photo] = photoRow
  386. let videoRow = sidebarItem("Video", icon: "􀎚", page: .video, logoImageName: "SidebarVideoLogo", logoIconWidth: 28, logoHeightMultiplier: 52.0 / 60.0)
  387. menuStack.addArrangedSubview(videoRow)
  388. sidebarRowViews[.video] = videoRow
  389. menuStack.addArrangedSubview(sidebarSectionTitle("Additional"))
  390. let tutorialsRow = sidebarItem("Tutorials", icon: "􀛩", page: .tutorials, logoImageName: "SidebarTutorialsLogo", logoIconWidth: 24, logoHeightMultiplier: 50.0 / 60.0, logoTemplate: false)
  391. menuStack.addArrangedSubview(tutorialsRow)
  392. sidebarRowViews[.tutorials] = tutorialsRow
  393. let settingsRow = sidebarItem("Settings", icon: "􀍟", page: .settings, logoImageName: "SidebarSettingsLogo", logoIconWidth: 28, logoHeightMultiplier: 68.0 / 62.0, showsDisclosure: true)
  394. menuStack.addArrangedSubview(settingsRow)
  395. sidebarRowViews[.settings] = settingsRow
  396. let premiumButton = sidebarPremiumButton()
  397. sidebar.addSubview(titleRow)
  398. sidebar.addSubview(menuStack)
  399. sidebar.addSubview(premiumButton)
  400. NSLayoutConstraint.activate([
  401. titleRow.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 16),
  402. titleRow.topAnchor.constraint(equalTo: sidebar.topAnchor, constant: 24),
  403. titleRow.trailingAnchor.constraint(lessThanOrEqualTo: sidebar.trailingAnchor, constant: -16),
  404. menuStack.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12),
  405. menuStack.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12),
  406. menuStack.topAnchor.constraint(equalTo: titleRow.bottomAnchor, constant: 20),
  407. menuStack.bottomAnchor.constraint(lessThanOrEqualTo: premiumButton.topAnchor, constant: -16),
  408. premiumButton.leadingAnchor.constraint(equalTo: sidebar.leadingAnchor, constant: 12),
  409. premiumButton.trailingAnchor.constraint(equalTo: sidebar.trailingAnchor, constant: -12),
  410. premiumButton.bottomAnchor.constraint(equalTo: sidebar.bottomAnchor, constant: -14)
  411. ])
  412. for subview in menuStack.arrangedSubviews {
  413. subview.widthAnchor.constraint(equalTo: menuStack.widthAnchor).isActive = true
  414. }
  415. return sidebar
  416. }
  417. func sidebarPremiumButton() -> NSView {
  418. let button = HoverTrackingView()
  419. button.translatesAutoresizingMaskIntoConstraints = false
  420. button.wantsLayer = true
  421. button.layer?.cornerRadius = 17
  422. button.layer?.backgroundColor = palette.primaryBlue.cgColor
  423. button.heightAnchor.constraint(equalToConstant: 34).isActive = true
  424. styleSurface(button, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: false)
  425. let icon = textLabel("★", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: .white)
  426. let title = textLabel("Get Premium", font: NSFont.systemFont(ofSize: 14, weight: .semibold), color: .white)
  427. button.addSubview(icon)
  428. button.addSubview(title)
  429. NSLayoutConstraint.activate([
  430. icon.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: 12),
  431. icon.centerYAnchor.constraint(equalTo: button.centerYAnchor),
  432. title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 8),
  433. title.centerYAnchor.constraint(equalTo: button.centerYAnchor),
  434. title.trailingAnchor.constraint(lessThanOrEqualTo: button.trailingAnchor, constant: -12)
  435. ])
  436. let baseColor = palette.primaryBlue
  437. let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
  438. button.onHoverChanged = { hovering in
  439. button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  440. }
  441. button.onHoverChanged?(false)
  442. let click = NSClickGestureRecognizer(target: self, action: #selector(premiumButtonClicked(_:)))
  443. button.addGestureRecognizer(click)
  444. return button
  445. }
  446. func makeMainPanel() -> NSView {
  447. let panel = NSView()
  448. panel.translatesAutoresizingMaskIntoConstraints = false
  449. panel.wantsLayer = true
  450. panel.layer?.backgroundColor = palette.pageBackground.cgColor
  451. let host = NSView()
  452. host.translatesAutoresizingMaskIntoConstraints = false
  453. panel.addSubview(host)
  454. NSLayoutConstraint.activate([
  455. host.leadingAnchor.constraint(equalTo: panel.leadingAnchor),
  456. host.trailingAnchor.constraint(equalTo: panel.trailingAnchor),
  457. host.topAnchor.constraint(equalTo: panel.topAnchor),
  458. host.bottomAnchor.constraint(equalTo: panel.bottomAnchor)
  459. ])
  460. mainContentHost = host
  461. showSidebarPage(.joinMeetings)
  462. return panel
  463. }
  464. func makeJoinMeetingsContent() -> NSView {
  465. let panel = NSView()
  466. panel.translatesAutoresizingMaskIntoConstraints = false
  467. let contentStack = NSStackView()
  468. contentStack.translatesAutoresizingMaskIntoConstraints = false
  469. contentStack.orientation = .vertical
  470. contentStack.spacing = 14
  471. contentStack.alignment = .leading
  472. let joinActions = meetJoinActionsRow()
  473. contentStack.addArrangedSubview(textLabel("Join Meetings", font: typography.pageTitle, color: palette.textPrimary))
  474. contentStack.addArrangedSubview(meetJoinSectionRow())
  475. contentStack.addArrangedSubview(joinActions)
  476. contentStack.setCustomSpacing(26, after: joinActions)
  477. contentStack.addArrangedSubview(scheduleHeader())
  478. contentStack.addArrangedSubview(textLabel("Tuesday, 14 Apr", font: typography.dateHeading, color: palette.textSecondary))
  479. contentStack.addArrangedSubview(scheduleCardsRow())
  480. panel.addSubview(contentStack)
  481. NSLayoutConstraint.activate([
  482. contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 28),
  483. contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -28),
  484. contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 26)
  485. ])
  486. return panel
  487. }
  488. func meetJoinSectionRow() -> NSView {
  489. let row = NSStackView()
  490. row.translatesAutoresizingMaskIntoConstraints = false
  491. row.orientation = .horizontal
  492. row.spacing = 12
  493. row.alignment = .top
  494. row.distribution = .fillEqually
  495. row.widthAnchor.constraint(greaterThanOrEqualToConstant: 880).isActive = true
  496. row.heightAnchor.constraint(equalToConstant: 140).isActive = true
  497. let instant = HoverSurfaceView()
  498. instant.translatesAutoresizingMaskIntoConstraints = false
  499. instant.wantsLayer = true
  500. instant.layer?.cornerRadius = 14
  501. instant.layer?.backgroundColor = palette.sectionCard.cgColor
  502. styleSurface(instant, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  503. let iconWrap = roundedContainer(cornerRadius: 12, color: NSColor.clear)
  504. iconWrap.translatesAutoresizingMaskIntoConstraints = false
  505. iconWrap.widthAnchor.constraint(equalToConstant: 58).isActive = true
  506. iconWrap.heightAnchor.constraint(equalToConstant: 58).isActive = true
  507. iconWrap.layer?.borderWidth = 0
  508. let meetLogoImage = NSImage(named: "MeetLogo") ?? NSImage()
  509. meetLogoImage.isTemplate = false
  510. let meetLogo = NSImageView(image: meetLogoImage)
  511. meetLogo.translatesAutoresizingMaskIntoConstraints = false
  512. meetLogo.imageScaling = .scaleProportionallyDown
  513. meetLogo.contentTintColor = nil
  514. iconWrap.addSubview(meetLogo)
  515. let instantTitle = textLabel("New Instant Meet", font: NSFont.systemFont(ofSize: 40 / 2, weight: .semibold), color: palette.textPrimary)
  516. let instantSub = textLabel("Start instant Meet in more section with\nGoogle Meet meet.", font: NSFont.systemFont(ofSize: 16 / 2, weight: .medium), color: palette.textSecondary)
  517. instantSub.maximumNumberOfLines = 2
  518. instant.addSubview(iconWrap)
  519. instant.addSubview(instantTitle)
  520. instant.addSubview(instantSub)
  521. let codeCard = HoverSurfaceView()
  522. codeCard.translatesAutoresizingMaskIntoConstraints = false
  523. codeCard.wantsLayer = true
  524. codeCard.layer?.cornerRadius = 14
  525. codeCard.layer?.backgroundColor = palette.sectionCard.cgColor
  526. styleSurface(codeCard, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  527. let codeTitle = textLabel("Join with Link", font: NSFont.systemFont(ofSize: 40 / 2, weight: .semibold), color: palette.textPrimary)
  528. let codeInputShell = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
  529. codeInputShell.translatesAutoresizingMaskIntoConstraints = false
  530. codeInputShell.heightAnchor.constraint(equalToConstant: 52).isActive = true
  531. styleSurface(codeInputShell, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  532. let codeField = NSTextField(string: "")
  533. codeField.translatesAutoresizingMaskIntoConstraints = false
  534. codeField.isEditable = true
  535. codeField.isBordered = false
  536. codeField.drawsBackground = false
  537. codeField.focusRingType = .none
  538. codeField.font = NSFont.systemFont(ofSize: 36 / 2, weight: .regular)
  539. codeField.textColor = palette.textPrimary
  540. codeField.placeholderString = "Enter Link"
  541. codeInputShell.addSubview(codeField)
  542. codeCard.addSubview(codeTitle)
  543. codeCard.addSubview(codeInputShell)
  544. NSLayoutConstraint.activate([
  545. meetLogo.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor),
  546. meetLogo.centerYAnchor.constraint(equalTo: iconWrap.centerYAnchor),
  547. meetLogo.widthAnchor.constraint(equalToConstant: 46),
  548. meetLogo.heightAnchor.constraint(equalToConstant: 46),
  549. iconWrap.leadingAnchor.constraint(equalTo: instant.leadingAnchor, constant: 18),
  550. iconWrap.topAnchor.constraint(equalTo: instant.topAnchor, constant: 22),
  551. instantTitle.leadingAnchor.constraint(equalTo: iconWrap.trailingAnchor, constant: 14),
  552. instantTitle.topAnchor.constraint(equalTo: instant.topAnchor, constant: 24),
  553. instantSub.leadingAnchor.constraint(equalTo: instantTitle.leadingAnchor),
  554. instantSub.topAnchor.constraint(equalTo: instantTitle.bottomAnchor, constant: 6),
  555. instantSub.trailingAnchor.constraint(lessThanOrEqualTo: instant.trailingAnchor, constant: -16),
  556. codeTitle.leadingAnchor.constraint(equalTo: codeCard.leadingAnchor, constant: 18),
  557. codeTitle.topAnchor.constraint(equalTo: codeCard.topAnchor, constant: 22),
  558. codeInputShell.leadingAnchor.constraint(equalTo: codeCard.leadingAnchor, constant: 18),
  559. codeInputShell.trailingAnchor.constraint(equalTo: codeCard.trailingAnchor, constant: -18),
  560. codeInputShell.topAnchor.constraint(equalTo: codeTitle.bottomAnchor, constant: 12),
  561. codeField.leadingAnchor.constraint(equalTo: codeInputShell.leadingAnchor, constant: 14),
  562. codeField.trailingAnchor.constraint(equalTo: codeInputShell.trailingAnchor, constant: -14),
  563. codeField.centerYAnchor.constraint(equalTo: codeInputShell.centerYAnchor)
  564. ])
  565. let baseColor = palette.sectionCard
  566. let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
  567. instant.onHoverChanged = { hovering in
  568. instant.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  569. }
  570. codeCard.onHoverChanged = { hovering in
  571. codeCard.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  572. }
  573. instant.onHoverChanged?(false)
  574. codeCard.onHoverChanged?(false)
  575. row.addArrangedSubview(instant)
  576. row.addArrangedSubview(codeCard)
  577. return row
  578. }
  579. func meetJoinActionsRow() -> NSView {
  580. let row = NSStackView()
  581. row.translatesAutoresizingMaskIntoConstraints = false
  582. row.orientation = .horizontal
  583. row.spacing = 12
  584. row.alignment = .centerY
  585. row.widthAnchor.constraint(greaterThanOrEqualToConstant: 880).isActive = true
  586. let spacer = NSView()
  587. spacer.translatesAutoresizingMaskIntoConstraints = false
  588. row.addArrangedSubview(spacer)
  589. row.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
  590. row.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
  591. return row
  592. }
  593. func makePaywallContent() -> NSView {
  594. paywallPlanViews.removeAll()
  595. premiumPlanByView.removeAll()
  596. let panel = NSView()
  597. panel.translatesAutoresizingMaskIntoConstraints = false
  598. panel.wantsLayer = true
  599. panel.layer?.backgroundColor = palette.pageBackground.cgColor
  600. let contentStack = NSStackView()
  601. contentStack.translatesAutoresizingMaskIntoConstraints = false
  602. contentStack.orientation = .vertical
  603. contentStack.spacing = 12
  604. contentStack.alignment = .leading
  605. panel.addSubview(contentStack)
  606. let topRow = NSStackView()
  607. topRow.translatesAutoresizingMaskIntoConstraints = false
  608. topRow.orientation = .horizontal
  609. topRow.alignment = .centerY
  610. topRow.distribution = .fill
  611. topRow.spacing = 10
  612. topRow.addArrangedSubview(textLabel("Get Premium", font: NSFont.systemFont(ofSize: 24, weight: .bold), color: palette.textPrimary))
  613. let topSpacer = NSView()
  614. topSpacer.translatesAutoresizingMaskIntoConstraints = false
  615. topRow.addArrangedSubview(topSpacer)
  616. let closeButton = iconRoundButton("✕", size: 28)
  617. topRow.addArrangedSubview(closeButton)
  618. let closeClick = NSClickGestureRecognizer(target: self, action: #selector(closePaywallClicked(_:)))
  619. closeButton.addGestureRecognizer(closeClick)
  620. topRow.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
  621. contentStack.addArrangedSubview(topRow)
  622. contentStack.addArrangedSubview(textLabel("Upgrade to unlock premium features.", font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.textSecondary))
  623. let benefits = paywallBenefitsSection()
  624. contentStack.addArrangedSubview(benefits)
  625. contentStack.setCustomSpacing(18, after: benefits)
  626. let weeklyCard = paywallPlanCard(
  627. title: "Weekly",
  628. price: "Rs 1,100.00",
  629. badge: "Basic Deal",
  630. badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
  631. subtitle: nil,
  632. plan: .weekly,
  633. strikePrice: nil
  634. )
  635. contentStack.addArrangedSubview(weeklyCard)
  636. let monthlyCard = paywallPlanCard(
  637. title: "Monthly",
  638. price: "Rs 2,500.00",
  639. badge: "Free Trial",
  640. badgeColor: NSColor(calibratedRed: 0.19, green: 0.82, blue: 0.39, alpha: 1),
  641. subtitle: "625.00/week",
  642. plan: .monthly,
  643. strikePrice: nil
  644. )
  645. contentStack.addArrangedSubview(monthlyCard)
  646. let yearlyCard = paywallPlanCard(
  647. title: "Yearly",
  648. price: "Rs 9,900.00",
  649. badge: "Best Deal",
  650. badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
  651. subtitle: "190.38/week",
  652. plan: .yearly,
  653. strikePrice: nil
  654. )
  655. contentStack.addArrangedSubview(yearlyCard)
  656. let lifetimeCard = paywallPlanCard(
  657. title: "Lifetime",
  658. price: "Rs 14,900.00",
  659. badge: "Save 50%",
  660. badgeColor: NSColor(calibratedRed: 1.0, green: 0.60, blue: 0.20, alpha: 1),
  661. subtitle: nil,
  662. plan: .lifetime,
  663. strikePrice: "Rs 29,800.00"
  664. )
  665. contentStack.addArrangedSubview(lifetimeCard)
  666. updatePaywallPlanSelection()
  667. contentStack.setCustomSpacing(20, after: lifetimeCard)
  668. let offer = textLabel("Free for 3 Days then Rs 2,500.00/month", font: NSFont.systemFont(ofSize: 13, weight: .semibold), color: palette.textPrimary)
  669. offer.alignment = .center
  670. let offerWrap = NSView()
  671. offerWrap.translatesAutoresizingMaskIntoConstraints = false
  672. offerWrap.addSubview(offer)
  673. NSLayoutConstraint.activate([
  674. offerWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
  675. offer.centerXAnchor.constraint(equalTo: offerWrap.centerXAnchor),
  676. offer.topAnchor.constraint(equalTo: offerWrap.topAnchor, constant: 6),
  677. offer.bottomAnchor.constraint(equalTo: offerWrap.bottomAnchor, constant: -2)
  678. ])
  679. contentStack.addArrangedSubview(offerWrap)
  680. contentStack.setCustomSpacing(18, after: offerWrap)
  681. let continueButton = roundedContainer(cornerRadius: 14, color: palette.primaryBlue)
  682. continueButton.translatesAutoresizingMaskIntoConstraints = false
  683. continueButton.heightAnchor.constraint(equalToConstant: 44).isActive = true
  684. continueButton.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
  685. styleSurface(continueButton, borderColor: palette.primaryBlueBorder, borderWidth: 1, shadow: true)
  686. let continueLabel = textLabel("Continue", font: NSFont.systemFont(ofSize: 16, weight: .bold), color: .white)
  687. continueButton.addSubview(continueLabel)
  688. NSLayoutConstraint.activate([
  689. continueLabel.centerXAnchor.constraint(equalTo: continueButton.centerXAnchor),
  690. continueLabel.centerYAnchor.constraint(equalTo: continueButton.centerYAnchor)
  691. ])
  692. contentStack.addArrangedSubview(continueButton)
  693. contentStack.setCustomSpacing(16, after: continueButton)
  694. let secure = textLabel("Secured by Apple. Cancel anytime.", font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
  695. secure.alignment = .center
  696. let secureWrap = NSView()
  697. secureWrap.translatesAutoresizingMaskIntoConstraints = false
  698. secureWrap.addSubview(secure)
  699. NSLayoutConstraint.activate([
  700. secureWrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth),
  701. secure.centerXAnchor.constraint(equalTo: secureWrap.centerXAnchor),
  702. secure.topAnchor.constraint(equalTo: secureWrap.topAnchor, constant: 4),
  703. secure.bottomAnchor.constraint(equalTo: secureWrap.bottomAnchor, constant: -8)
  704. ])
  705. contentStack.addArrangedSubview(secureWrap)
  706. contentStack.setCustomSpacing(16, after: secureWrap)
  707. let footer = paywallFooterLinks()
  708. contentStack.addArrangedSubview(footer)
  709. NSLayoutConstraint.activate([
  710. contentStack.leadingAnchor.constraint(equalTo: panel.leadingAnchor, constant: 18),
  711. contentStack.trailingAnchor.constraint(equalTo: panel.trailingAnchor, constant: -18),
  712. contentStack.topAnchor.constraint(equalTo: panel.topAnchor, constant: 16),
  713. contentStack.bottomAnchor.constraint(lessThanOrEqualTo: panel.bottomAnchor, constant: -12)
  714. ])
  715. return panel
  716. }
  717. func paywallPlanCard(
  718. title: String,
  719. price: String,
  720. badge: String,
  721. badgeColor: NSColor,
  722. subtitle: String?,
  723. plan: PremiumPlan,
  724. strikePrice: String?
  725. ) -> NSView {
  726. let wrapper = NSView()
  727. wrapper.translatesAutoresizingMaskIntoConstraints = false
  728. wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
  729. wrapper.heightAnchor.constraint(equalToConstant: 94).isActive = true
  730. let card = roundedContainer(cornerRadius: 16, color: palette.sectionCard)
  731. card.translatesAutoresizingMaskIntoConstraints = false
  732. card.heightAnchor.constraint(equalToConstant: 82).isActive = true
  733. wrapper.addSubview(card)
  734. NSLayoutConstraint.activate([
  735. card.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  736. card.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  737. card.topAnchor.constraint(equalTo: wrapper.topAnchor, constant: 12),
  738. card.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
  739. ])
  740. styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  741. let badgeLabel = textLabel(badge, font: NSFont.systemFont(ofSize: 10, weight: .bold), color: .white)
  742. let badgeWrap = roundedContainer(cornerRadius: 10, color: badgeColor)
  743. badgeWrap.translatesAutoresizingMaskIntoConstraints = false
  744. badgeWrap.wantsLayer = true
  745. badgeWrap.layer?.borderColor = NSColor(calibratedWhite: 1, alpha: 0.22).cgColor
  746. badgeWrap.layer?.borderWidth = 1
  747. badgeWrap.layer?.shadowColor = NSColor.black.cgColor
  748. badgeWrap.layer?.shadowOpacity = 0.20
  749. badgeWrap.layer?.shadowOffset = CGSize(width: 0, height: -1)
  750. badgeWrap.layer?.shadowRadius = 3
  751. badgeWrap.addSubview(badgeLabel)
  752. NSLayoutConstraint.activate([
  753. badgeLabel.leadingAnchor.constraint(equalTo: badgeWrap.leadingAnchor, constant: 8),
  754. badgeLabel.trailingAnchor.constraint(equalTo: badgeWrap.trailingAnchor, constant: -8),
  755. badgeLabel.topAnchor.constraint(equalTo: badgeWrap.topAnchor, constant: 2),
  756. badgeLabel.bottomAnchor.constraint(equalTo: badgeWrap.bottomAnchor, constant: -2)
  757. ])
  758. wrapper.addSubview(badgeWrap)
  759. let titleLabel = textLabel(title, font: NSFont.systemFont(ofSize: 15, weight: .bold), color: palette.primaryBlue)
  760. card.addSubview(titleLabel)
  761. let priceLabel = textLabel(price, font: NSFont.systemFont(ofSize: 12, weight: .bold), color: palette.textPrimary)
  762. card.addSubview(priceLabel)
  763. NSLayoutConstraint.activate([
  764. badgeWrap.centerXAnchor.constraint(equalTo: card.centerXAnchor),
  765. badgeWrap.centerYAnchor.constraint(equalTo: card.topAnchor),
  766. titleLabel.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 16),
  767. titleLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 34),
  768. priceLabel.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -16),
  769. priceLabel.topAnchor.constraint(equalTo: card.topAnchor, constant: 32)
  770. ])
  771. if let subtitle {
  772. let sub = textLabel(subtitle, font: NSFont.systemFont(ofSize: 10, weight: .semibold), color: palette.textSecondary)
  773. card.addSubview(sub)
  774. NSLayoutConstraint.activate([
  775. sub.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
  776. sub.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 0)
  777. ])
  778. }
  779. if let strikePrice {
  780. let strike = textLabel(strikePrice, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: NSColor.systemRed)
  781. card.addSubview(strike)
  782. NSLayoutConstraint.activate([
  783. strike.trailingAnchor.constraint(equalTo: priceLabel.trailingAnchor),
  784. strike.topAnchor.constraint(equalTo: priceLabel.bottomAnchor, constant: 4)
  785. ])
  786. }
  787. let click = NSClickGestureRecognizer(target: self, action: #selector(paywallPlanClicked(_:)))
  788. wrapper.addGestureRecognizer(click)
  789. premiumPlanByView[ObjectIdentifier(wrapper)] = plan
  790. paywallPlanViews[plan] = card
  791. return wrapper
  792. }
  793. func paywallFooterLinks() -> NSView {
  794. let wrap = NSView()
  795. wrap.translatesAutoresizingMaskIntoConstraints = false
  796. wrap.heightAnchor.constraint(equalToConstant: 34).isActive = true
  797. wrap.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
  798. let row = NSStackView()
  799. row.translatesAutoresizingMaskIntoConstraints = false
  800. row.orientation = .horizontal
  801. row.distribution = .fillEqually
  802. row.alignment = .centerY
  803. row.spacing = 0
  804. wrap.addSubview(row)
  805. row.addArrangedSubview(footerLink("Privacy Policy"))
  806. row.addArrangedSubview(footerLink("Support"))
  807. row.addArrangedSubview(footerLink("Terms of Services"))
  808. NSLayoutConstraint.activate([
  809. row.leadingAnchor.constraint(equalTo: wrap.leadingAnchor),
  810. row.trailingAnchor.constraint(equalTo: wrap.trailingAnchor),
  811. row.topAnchor.constraint(equalTo: wrap.topAnchor),
  812. row.bottomAnchor.constraint(equalTo: wrap.bottomAnchor)
  813. ])
  814. return wrap
  815. }
  816. func footerLink(_ title: String) -> NSView {
  817. let container = HoverTrackingView()
  818. container.translatesAutoresizingMaskIntoConstraints = false
  819. let label = textLabel(title, font: NSFont.systemFont(ofSize: 12, weight: .semibold), color: palette.textSecondary)
  820. label.alignment = .center
  821. container.addSubview(label)
  822. NSLayoutConstraint.activate([
  823. label.centerXAnchor.constraint(equalTo: container.centerXAnchor),
  824. label.centerYAnchor.constraint(equalTo: container.centerYAnchor)
  825. ])
  826. let click = NSClickGestureRecognizer(target: self, action: #selector(paywallFooterLinkClicked(_:)))
  827. container.addGestureRecognizer(click)
  828. container.onHoverChanged = { hovering in
  829. label.textColor = hovering ? .white : self.palette.textSecondary
  830. }
  831. container.onHoverChanged?(false)
  832. return container
  833. }
  834. func paywallBenefitsSection() -> NSView {
  835. let stack = NSStackView()
  836. stack.translatesAutoresizingMaskIntoConstraints = false
  837. stack.orientation = .vertical
  838. stack.spacing = 8
  839. stack.alignment = .leading
  840. stack.widthAnchor.constraint(greaterThanOrEqualToConstant: paywallContentWidth).isActive = true
  841. let rowOne = NSStackView()
  842. rowOne.translatesAutoresizingMaskIntoConstraints = false
  843. rowOne.orientation = .horizontal
  844. rowOne.spacing = 10
  845. rowOne.distribution = .fillEqually
  846. rowOne.alignment = .centerY
  847. rowOne.addArrangedSubview(paywallBenefitItem(icon: "📅", text: "Manage meetings"))
  848. rowOne.addArrangedSubview(paywallBenefitItem(icon: "🖼️", text: "Virtual backgrounds"))
  849. let rowTwo = NSStackView()
  850. rowTwo.translatesAutoresizingMaskIntoConstraints = false
  851. rowTwo.orientation = .horizontal
  852. rowTwo.spacing = 10
  853. rowTwo.distribution = .fillEqually
  854. rowTwo.alignment = .centerY
  855. rowTwo.addArrangedSubview(paywallBenefitItem(icon: "⚡", text: "Tools for productivity"))
  856. rowTwo.addArrangedSubview(paywallBenefitItem(icon: "🛟", text: "24/7 support"))
  857. stack.addArrangedSubview(rowOne)
  858. stack.addArrangedSubview(rowTwo)
  859. return stack
  860. }
  861. func paywallBenefitItem(icon: String, text: String) -> NSView {
  862. let card = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
  863. card.translatesAutoresizingMaskIntoConstraints = false
  864. card.heightAnchor.constraint(equalToConstant: 36).isActive = true
  865. styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  866. let iconWrap = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
  867. iconWrap.translatesAutoresizingMaskIntoConstraints = false
  868. iconWrap.widthAnchor.constraint(equalToConstant: 24).isActive = true
  869. iconWrap.heightAnchor.constraint(equalToConstant: 24).isActive = true
  870. styleSurface(iconWrap, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  871. let iconLabel = textLabel(icon, font: NSFont.systemFont(ofSize: 12, weight: .medium), color: palette.primaryBlue)
  872. iconWrap.addSubview(iconLabel)
  873. NSLayoutConstraint.activate([
  874. iconLabel.centerXAnchor.constraint(equalTo: iconWrap.centerXAnchor),
  875. iconLabel.centerYAnchor.constraint(equalTo: iconWrap.centerYAnchor)
  876. ])
  877. let title = textLabel(text, font: NSFont.systemFont(ofSize: 11, weight: .medium), color: palette.textPrimary)
  878. card.addSubview(iconWrap)
  879. card.addSubview(title)
  880. NSLayoutConstraint.activate([
  881. iconWrap.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 8),
  882. iconWrap.centerYAnchor.constraint(equalTo: card.centerYAnchor),
  883. title.leadingAnchor.constraint(equalTo: iconWrap.trailingAnchor, constant: 10),
  884. title.centerYAnchor.constraint(equalTo: card.centerYAnchor),
  885. title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -8)
  886. ])
  887. return card
  888. }
  889. func zoomJoinModeTabs() -> NSView {
  890. let row = NSStackView()
  891. row.translatesAutoresizingMaskIntoConstraints = false
  892. row.orientation = .horizontal
  893. row.alignment = .centerY
  894. row.spacing = 28
  895. row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
  896. let idTab = joinModeTab("Join with ID", mode: .id)
  897. let urlTab = joinModeTab("Join with URL", mode: .url)
  898. row.addArrangedSubview(idTab)
  899. row.addArrangedSubview(urlTab)
  900. let spacer = NSView()
  901. spacer.translatesAutoresizingMaskIntoConstraints = false
  902. row.addArrangedSubview(spacer)
  903. zoomJoinModeViews[.id] = idTab
  904. zoomJoinModeViews[.url] = urlTab
  905. updateZoomJoinModeAppearance()
  906. return row
  907. }
  908. func joinModeTab(_ title: String, mode: ZoomJoinMode) -> NSView {
  909. let tab = HoverTrackingView()
  910. tab.translatesAutoresizingMaskIntoConstraints = false
  911. tab.wantsLayer = true
  912. tab.layer?.cornerRadius = 6
  913. tab.layer?.backgroundColor = NSColor.clear.cgColor
  914. tab.heightAnchor.constraint(equalToConstant: 30).isActive = true
  915. zoomJoinModeByView[ObjectIdentifier(tab)] = mode
  916. let label = textLabel(title, font: NSFont.systemFont(ofSize: 33 / 2, weight: .medium), color: palette.textPrimary)
  917. tab.addSubview(label)
  918. NSLayoutConstraint.activate([
  919. label.leadingAnchor.constraint(equalTo: tab.leadingAnchor, constant: 4),
  920. label.trailingAnchor.constraint(equalTo: tab.trailingAnchor, constant: -4),
  921. label.topAnchor.constraint(equalTo: tab.topAnchor, constant: 4),
  922. label.bottomAnchor.constraint(equalTo: tab.bottomAnchor, constant: -6)
  923. ])
  924. let click = NSClickGestureRecognizer(target: self, action: #selector(zoomJoinModeClicked(_:)))
  925. tab.addGestureRecognizer(click)
  926. return tab
  927. }
  928. func updateZoomJoinModeAppearance() {
  929. for (mode, tab) in zoomJoinModeViews {
  930. let selected = (mode == selectedZoomJoinMode)
  931. let textColor = selected ? palette.textPrimary : palette.textSecondary
  932. let label = tab.subviews.first { $0 is NSTextField } as? NSTextField
  933. label?.textColor = textColor
  934. // Keep the active tab visually underlined like the reference.
  935. if selected {
  936. if tab.subviews.contains(where: { $0.identifier?.rawValue == "modeUnderline" }) == false {
  937. let underline = NSView()
  938. underline.identifier = NSUserInterfaceItemIdentifier("modeUnderline")
  939. underline.translatesAutoresizingMaskIntoConstraints = false
  940. underline.wantsLayer = true
  941. underline.layer?.backgroundColor = palette.primaryBlue.cgColor
  942. tab.addSubview(underline)
  943. NSLayoutConstraint.activate([
  944. underline.leadingAnchor.constraint(equalTo: tab.leadingAnchor),
  945. underline.trailingAnchor.constraint(equalTo: tab.trailingAnchor),
  946. underline.bottomAnchor.constraint(equalTo: tab.bottomAnchor),
  947. underline.heightAnchor.constraint(equalToConstant: 2)
  948. ])
  949. }
  950. } else {
  951. tab.subviews
  952. .filter { $0.identifier?.rawValue == "modeUnderline" }
  953. .forEach { $0.removeFromSuperview() }
  954. }
  955. }
  956. }
  957. func joinWithIDHeading() -> NSView {
  958. let container = NSView()
  959. container.translatesAutoresizingMaskIntoConstraints = false
  960. let title = textLabel("Join with ID", font: typography.joinWithURLTitle, color: palette.textPrimary)
  961. title.alignment = .left
  962. title.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  963. title.setContentCompressionResistancePriority(.required, for: .horizontal)
  964. let bar = NSView()
  965. bar.translatesAutoresizingMaskIntoConstraints = false
  966. bar.wantsLayer = true
  967. bar.layer?.backgroundColor = palette.primaryBlue.cgColor
  968. bar.heightAnchor.constraint(equalToConstant: 3).isActive = true
  969. container.addSubview(title)
  970. container.addSubview(bar)
  971. NSLayoutConstraint.activate([
  972. title.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  973. title.topAnchor.constraint(equalTo: container.topAnchor),
  974. bar.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  975. bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
  976. bar.widthAnchor.constraint(equalTo: title.widthAnchor),
  977. bar.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  978. container.trailingAnchor.constraint(equalTo: title.trailingAnchor)
  979. ])
  980. return container
  981. }
  982. func zoomMeetingIDSection() -> NSView {
  983. let wrapper = NSView()
  984. wrapper.translatesAutoresizingMaskIntoConstraints = false
  985. let fieldsRow = NSStackView()
  986. fieldsRow.translatesAutoresizingMaskIntoConstraints = false
  987. fieldsRow.orientation = .horizontal
  988. fieldsRow.alignment = .top
  989. fieldsRow.distribution = .fillEqually
  990. fieldsRow.spacing = 12
  991. fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting ID", placeholder: "Enter meeting ID..."))
  992. fieldsRow.addArrangedSubview(zoomInputField(title: "Meeting Passcode", placeholder: "Enter meeting passcode..."))
  993. let actions = NSStackView()
  994. actions.orientation = .horizontal
  995. actions.spacing = 10
  996. actions.translatesAutoresizingMaskIntoConstraints = false
  997. actions.alignment = .centerY
  998. actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
  999. actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
  1000. wrapper.addSubview(fieldsRow)
  1001. wrapper.addSubview(actions)
  1002. NSLayoutConstraint.activate([
  1003. wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
  1004. fieldsRow.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  1005. fieldsRow.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  1006. fieldsRow.topAnchor.constraint(equalTo: wrapper.topAnchor),
  1007. actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  1008. actions.topAnchor.constraint(equalTo: fieldsRow.bottomAnchor, constant: 14),
  1009. actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
  1010. ])
  1011. return wrapper
  1012. }
  1013. func zoomInputField(title: String, placeholder: String) -> NSView {
  1014. let wrapper = NSView()
  1015. wrapper.translatesAutoresizingMaskIntoConstraints = false
  1016. let heading = textLabel(title, font: typography.fieldLabel, color: palette.textPrimary)
  1017. let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
  1018. textFieldContainer.translatesAutoresizingMaskIntoConstraints = false
  1019. textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true
  1020. styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  1021. let field = NSTextField(string: "")
  1022. field.translatesAutoresizingMaskIntoConstraints = false
  1023. field.isEditable = true
  1024. field.isSelectable = true
  1025. field.isBordered = false
  1026. field.drawsBackground = false
  1027. field.placeholderString = placeholder
  1028. field.font = typography.inputPlaceholder
  1029. field.textColor = palette.textPrimary
  1030. field.focusRingType = .none
  1031. textFieldContainer.addSubview(field)
  1032. wrapper.addSubview(heading)
  1033. wrapper.addSubview(textFieldContainer)
  1034. NSLayoutConstraint.activate([
  1035. heading.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  1036. heading.topAnchor.constraint(equalTo: wrapper.topAnchor),
  1037. textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  1038. textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  1039. textFieldContainer.topAnchor.constraint(equalTo: heading.bottomAnchor, constant: 10),
  1040. textFieldContainer.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor),
  1041. field.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12),
  1042. field.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12),
  1043. field.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor)
  1044. ])
  1045. return wrapper
  1046. }
  1047. func joinWithURLHeading() -> NSView {
  1048. let container = NSView()
  1049. container.translatesAutoresizingMaskIntoConstraints = false
  1050. let title = textLabel("Join with URL", font: typography.joinWithURLTitle, color: palette.textPrimary)
  1051. title.alignment = .left
  1052. title.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  1053. title.setContentCompressionResistancePriority(.required, for: .horizontal)
  1054. let bar = NSView()
  1055. bar.translatesAutoresizingMaskIntoConstraints = false
  1056. bar.wantsLayer = true
  1057. bar.layer?.backgroundColor = palette.primaryBlue.cgColor
  1058. bar.heightAnchor.constraint(equalToConstant: 3).isActive = true
  1059. container.addSubview(title)
  1060. container.addSubview(bar)
  1061. NSLayoutConstraint.activate([
  1062. title.leadingAnchor.constraint(equalTo: container.leadingAnchor),
  1063. title.topAnchor.constraint(equalTo: container.topAnchor),
  1064. bar.leadingAnchor.constraint(equalTo: title.leadingAnchor),
  1065. bar.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6),
  1066. bar.widthAnchor.constraint(equalTo: title.widthAnchor),
  1067. bar.bottomAnchor.constraint(equalTo: container.bottomAnchor),
  1068. container.trailingAnchor.constraint(equalTo: title.trailingAnchor)
  1069. ])
  1070. return container
  1071. }
  1072. func meetingUrlSection() -> NSView {
  1073. let wrapper = NSView()
  1074. wrapper.translatesAutoresizingMaskIntoConstraints = false
  1075. let title = textLabel("Meeting URL", font: typography.fieldLabel, color: palette.textSecondary)
  1076. let textFieldContainer = roundedContainer(cornerRadius: 10, color: palette.inputBackground)
  1077. textFieldContainer.translatesAutoresizingMaskIntoConstraints = false
  1078. textFieldContainer.heightAnchor.constraint(equalToConstant: 40).isActive = true
  1079. styleSurface(textFieldContainer, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  1080. let urlField = NSTextField(string: "")
  1081. urlField.translatesAutoresizingMaskIntoConstraints = false
  1082. urlField.isEditable = true
  1083. urlField.isSelectable = true
  1084. urlField.isBordered = false
  1085. urlField.drawsBackground = false
  1086. urlField.placeholderString = "Enter meeting URL..."
  1087. urlField.font = typography.inputPlaceholder
  1088. urlField.textColor = palette.textPrimary
  1089. urlField.focusRingType = .none
  1090. textFieldContainer.addSubview(urlField)
  1091. let actions = NSStackView()
  1092. actions.orientation = .horizontal
  1093. actions.spacing = 10
  1094. actions.translatesAutoresizingMaskIntoConstraints = false
  1095. actions.alignment = .centerY
  1096. actions.addArrangedSubview(actionButton(title: "Cancel", color: palette.cancelButton, textColor: palette.textSecondary, width: 110))
  1097. actions.addArrangedSubview(actionButton(title: "Join", color: palette.primaryBlue, textColor: .white, width: 116))
  1098. wrapper.addSubview(title)
  1099. wrapper.addSubview(textFieldContainer)
  1100. wrapper.addSubview(actions)
  1101. NSLayoutConstraint.activate([
  1102. wrapper.widthAnchor.constraint(greaterThanOrEqualToConstant: 780),
  1103. title.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  1104. title.topAnchor.constraint(equalTo: wrapper.topAnchor),
  1105. textFieldContainer.leadingAnchor.constraint(equalTo: wrapper.leadingAnchor),
  1106. textFieldContainer.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  1107. textFieldContainer.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 10),
  1108. urlField.leadingAnchor.constraint(equalTo: textFieldContainer.leadingAnchor, constant: 12),
  1109. urlField.trailingAnchor.constraint(equalTo: textFieldContainer.trailingAnchor, constant: -12),
  1110. urlField.centerYAnchor.constraint(equalTo: textFieldContainer.centerYAnchor),
  1111. actions.trailingAnchor.constraint(equalTo: wrapper.trailingAnchor),
  1112. actions.topAnchor.constraint(equalTo: textFieldContainer.bottomAnchor, constant: 14),
  1113. actions.bottomAnchor.constraint(equalTo: wrapper.bottomAnchor)
  1114. ])
  1115. return wrapper
  1116. }
  1117. func scheduleHeader() -> NSView {
  1118. let row = NSStackView()
  1119. row.translatesAutoresizingMaskIntoConstraints = false
  1120. row.orientation = .horizontal
  1121. row.alignment = .centerY
  1122. row.distribution = .fill
  1123. row.spacing = 12
  1124. row.addArrangedSubview(textLabel("Schedule", font: typography.sectionTitleBold, color: palette.textPrimary))
  1125. let spacer = NSView()
  1126. spacer.translatesAutoresizingMaskIntoConstraints = false
  1127. row.addArrangedSubview(spacer)
  1128. spacer.setContentHuggingPriority(.defaultLow, for: .horizontal)
  1129. row.addArrangedSubview(iconRoundButton("?", size: 34))
  1130. row.addArrangedSubview(iconRoundButton("⟳", size: 34))
  1131. let filter = roundedContainer(cornerRadius: 8, color: palette.inputBackground)
  1132. filter.translatesAutoresizingMaskIntoConstraints = false
  1133. filter.widthAnchor.constraint(equalToConstant: 156).isActive = true
  1134. filter.heightAnchor.constraint(equalToConstant: 34).isActive = true
  1135. styleSurface(filter, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  1136. let filterText = textLabel("All", font: typography.filterText, color: palette.textSecondary)
  1137. let arrow = textLabel("▾", font: typography.filterArrow, color: palette.textMuted)
  1138. filterText.translatesAutoresizingMaskIntoConstraints = false
  1139. arrow.translatesAutoresizingMaskIntoConstraints = false
  1140. filter.addSubview(filterText)
  1141. filter.addSubview(arrow)
  1142. NSLayoutConstraint.activate([
  1143. filterText.leadingAnchor.constraint(equalTo: filter.leadingAnchor, constant: 12),
  1144. filterText.centerYAnchor.constraint(equalTo: filter.centerYAnchor),
  1145. arrow.trailingAnchor.constraint(equalTo: filter.trailingAnchor, constant: -10),
  1146. arrow.centerYAnchor.constraint(equalTo: filter.centerYAnchor)
  1147. ])
  1148. row.addArrangedSubview(filter)
  1149. row.widthAnchor.constraint(greaterThanOrEqualToConstant: 780).isActive = true
  1150. return row
  1151. }
  1152. func scheduleCardsRow() -> NSView {
  1153. let row = NSStackView()
  1154. row.translatesAutoresizingMaskIntoConstraints = false
  1155. row.orientation = .horizontal
  1156. row.spacing = 10
  1157. row.alignment = .top
  1158. row.distribution = .fill
  1159. row.setContentHuggingPriority(.defaultHigh, for: .horizontal)
  1160. row.heightAnchor.constraint(equalToConstant: 136).isActive = true
  1161. row.addArrangedSubview(scheduleCard())
  1162. row.addArrangedSubview(scheduleCard())
  1163. return row
  1164. }
  1165. func scheduleCard() -> NSView {
  1166. let cardWidth: CGFloat = 264
  1167. let card = roundedContainer(cornerRadius: 10, color: palette.sectionCard)
  1168. styleSurface(card, borderColor: palette.inputBorder, borderWidth: 1, shadow: true)
  1169. card.translatesAutoresizingMaskIntoConstraints = false
  1170. card.widthAnchor.constraint(equalToConstant: cardWidth).isActive = true
  1171. card.heightAnchor.constraint(equalToConstant: 136).isActive = true
  1172. let icon = roundedContainer(cornerRadius: 5, color: palette.meetingBadge)
  1173. icon.translatesAutoresizingMaskIntoConstraints = false
  1174. icon.widthAnchor.constraint(equalToConstant: 22).isActive = true
  1175. icon.heightAnchor.constraint(equalToConstant: 22).isActive = true
  1176. let iconText = textLabel("••", font: typography.cardIcon, color: .white)
  1177. iconText.translatesAutoresizingMaskIntoConstraints = false
  1178. icon.addSubview(iconText)
  1179. NSLayoutConstraint.activate([
  1180. iconText.centerXAnchor.constraint(equalTo: icon.centerXAnchor),
  1181. iconText.centerYAnchor.constraint(equalTo: icon.centerYAnchor)
  1182. ])
  1183. let title = textLabel("General Meeting", font: typography.cardTitle, color: palette.textPrimary)
  1184. let subtitle = textLabel("Baisakhi", font: typography.cardSubtitle, color: palette.textPrimary)
  1185. let time = textLabel("12:00 AM - 11:59 PM", font: typography.cardTime, color: palette.textSecondary)
  1186. card.addSubview(icon)
  1187. card.addSubview(title)
  1188. card.addSubview(subtitle)
  1189. card.addSubview(time)
  1190. NSLayoutConstraint.activate([
  1191. icon.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  1192. icon.topAnchor.constraint(equalTo: card.topAnchor, constant: 10),
  1193. title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 6),
  1194. title.centerYAnchor.constraint(equalTo: icon.centerYAnchor),
  1195. title.trailingAnchor.constraint(lessThanOrEqualTo: card.trailingAnchor, constant: -10),
  1196. subtitle.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  1197. subtitle.topAnchor.constraint(equalTo: icon.bottomAnchor, constant: 7),
  1198. time.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 10),
  1199. time.topAnchor.constraint(equalTo: subtitle.bottomAnchor, constant: 4)
  1200. ])
  1201. return card
  1202. }
  1203. }
  1204. /// Ensures `NSClickGestureRecognizer` on the row receives clicks instead of child label/image views swallowing them.
  1205. private class RowHitTestView: NSView {
  1206. override func hitTest(_ point: NSPoint) -> NSView? {
  1207. guard let superview else { return nil }
  1208. let local = convert(point, from: superview)
  1209. return bounds.contains(local) ? self : nil
  1210. }
  1211. }
  1212. private final class HoverTrackingView: RowHitTestView {
  1213. var onHoverChanged: ((Bool) -> Void)?
  1214. var showsHandCursor = true
  1215. private var trackingAreaRef: NSTrackingArea?
  1216. private var isHovering = false {
  1217. didSet {
  1218. guard isHovering != oldValue else { return }
  1219. onHoverChanged?(isHovering)
  1220. }
  1221. }
  1222. override func updateTrackingAreas() {
  1223. super.updateTrackingAreas()
  1224. if let trackingAreaRef {
  1225. removeTrackingArea(trackingAreaRef)
  1226. }
  1227. let options: NSTrackingArea.Options = [
  1228. .activeInKeyWindow,
  1229. .inVisibleRect,
  1230. .mouseEnteredAndExited
  1231. ]
  1232. let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
  1233. addTrackingArea(area)
  1234. trackingAreaRef = area
  1235. }
  1236. override func mouseEntered(with event: NSEvent) {
  1237. super.mouseEntered(with: event)
  1238. isHovering = true
  1239. }
  1240. override func mouseExited(with event: NSEvent) {
  1241. super.mouseExited(with: event)
  1242. isHovering = false
  1243. }
  1244. override func resetCursorRects() {
  1245. super.resetCursorRects()
  1246. guard showsHandCursor else { return }
  1247. addCursorRect(bounds, cursor: .pointingHand)
  1248. }
  1249. }
  1250. /// Hover tracking without overriding hit-testing; keeps controls like text fields interactive.
  1251. private final class HoverSurfaceView: NSView {
  1252. var onHoverChanged: ((Bool) -> Void)?
  1253. private var trackingAreaRef: NSTrackingArea?
  1254. private var isHovering = false {
  1255. didSet {
  1256. guard isHovering != oldValue else { return }
  1257. onHoverChanged?(isHovering)
  1258. }
  1259. }
  1260. override func updateTrackingAreas() {
  1261. super.updateTrackingAreas()
  1262. if let trackingAreaRef {
  1263. removeTrackingArea(trackingAreaRef)
  1264. }
  1265. let options: NSTrackingArea.Options = [
  1266. .activeInKeyWindow,
  1267. .inVisibleRect,
  1268. .mouseEnteredAndExited
  1269. ]
  1270. let area = NSTrackingArea(rect: bounds, options: options, owner: self, userInfo: nil)
  1271. addTrackingArea(area)
  1272. trackingAreaRef = area
  1273. }
  1274. override func mouseEntered(with event: NSEvent) {
  1275. super.mouseEntered(with: event)
  1276. isHovering = true
  1277. }
  1278. override func mouseExited(with event: NSEvent) {
  1279. super.mouseExited(with: event)
  1280. isHovering = false
  1281. }
  1282. }
  1283. private final class SettingsMenuViewController: NSViewController {
  1284. private let palette: Palette
  1285. private let typography: Typography
  1286. private let onToggleDarkMode: (Bool) -> Void
  1287. private let onAction: (SettingsAction) -> Void
  1288. private var darkToggle: NSSwitch?
  1289. init(
  1290. palette: Palette,
  1291. typography: Typography,
  1292. darkModeEnabled: Bool,
  1293. onToggleDarkMode: @escaping (Bool) -> Void,
  1294. onAction: @escaping (SettingsAction) -> Void
  1295. ) {
  1296. self.palette = palette
  1297. self.typography = typography
  1298. self.onToggleDarkMode = onToggleDarkMode
  1299. self.onAction = onAction
  1300. super.init(nibName: nil, bundle: nil)
  1301. self.view = makeView(darkModeEnabled: darkModeEnabled)
  1302. }
  1303. @available(*, unavailable)
  1304. required init?(coder: NSCoder) {
  1305. nil
  1306. }
  1307. func setDarkModeEnabled(_ enabled: Bool) {
  1308. darkToggle?.state = enabled ? .on : .off
  1309. }
  1310. private func makeView(darkModeEnabled: Bool) -> NSView {
  1311. let root = NSView()
  1312. root.translatesAutoresizingMaskIntoConstraints = false
  1313. let card = roundedCard()
  1314. root.addSubview(card)
  1315. NSLayoutConstraint.activate([
  1316. card.leadingAnchor.constraint(equalTo: root.leadingAnchor),
  1317. card.trailingAnchor.constraint(equalTo: root.trailingAnchor),
  1318. card.topAnchor.constraint(equalTo: root.topAnchor),
  1319. card.bottomAnchor.constraint(equalTo: root.bottomAnchor),
  1320. root.widthAnchor.constraint(equalToConstant: 260)
  1321. ])
  1322. let stack = NSStackView()
  1323. stack.translatesAutoresizingMaskIntoConstraints = false
  1324. stack.orientation = .vertical
  1325. stack.spacing = 6
  1326. stack.alignment = .leading
  1327. card.addSubview(stack)
  1328. NSLayoutConstraint.activate([
  1329. stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
  1330. stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
  1331. stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
  1332. stack.bottomAnchor.constraint(lessThanOrEqualTo: card.bottomAnchor, constant: -14)
  1333. ])
  1334. stack.addArrangedSubview(settingsDarkModeRow(enabled: darkModeEnabled))
  1335. stack.addArrangedSubview(settingsActionRow(icon: "⟳", title: "Restore", action: .restore))
  1336. stack.addArrangedSubview(settingsActionRow(icon: "★", title: "Rate Us", action: .rateUs))
  1337. stack.addArrangedSubview(settingsActionRow(icon: "💬", title: "Support", action: .support))
  1338. stack.addArrangedSubview(settingsActionRow(icon: "⋯", title: "More Apps", action: .moreApps))
  1339. stack.addArrangedSubview(settingsActionRow(icon: "⤴︎", title: "Share App", action: .shareApp))
  1340. for v in stack.arrangedSubviews {
  1341. v.widthAnchor.constraint(equalTo: stack.widthAnchor).isActive = true
  1342. }
  1343. return root
  1344. }
  1345. private func roundedCard() -> NSView {
  1346. let view = NSView()
  1347. view.translatesAutoresizingMaskIntoConstraints = false
  1348. view.wantsLayer = true
  1349. view.layer?.cornerRadius = 12
  1350. view.layer?.backgroundColor = NSColor(calibratedWhite: 0.12, alpha: 1).cgColor
  1351. view.layer?.borderColor = NSColor(calibratedWhite: 0.22, alpha: 1).cgColor
  1352. view.layer?.borderWidth = 1
  1353. view.layer?.shadowColor = NSColor.black.cgColor
  1354. view.layer?.shadowOpacity = 0.28
  1355. view.layer?.shadowOffset = CGSize(width: 0, height: -1)
  1356. view.layer?.shadowRadius = 10
  1357. return view
  1358. }
  1359. private func settingsDarkModeRow(enabled: Bool) -> NSView {
  1360. let row = HoverTrackingView()
  1361. row.translatesAutoresizingMaskIntoConstraints = false
  1362. row.heightAnchor.constraint(equalToConstant: 44).isActive = true
  1363. let icon = NSTextField(labelWithString: "◐")
  1364. icon.translatesAutoresizingMaskIntoConstraints = false
  1365. icon.font = NSFont.systemFont(ofSize: 18, weight: .medium)
  1366. icon.textColor = .white
  1367. let title = NSTextField(labelWithString: "Dark Mode")
  1368. title.translatesAutoresizingMaskIntoConstraints = false
  1369. title.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
  1370. title.textColor = .white
  1371. let toggle = NSSwitch()
  1372. toggle.translatesAutoresizingMaskIntoConstraints = false
  1373. toggle.state = enabled ? .on : .off
  1374. toggle.target = self
  1375. toggle.action = #selector(darkModeToggled(_:))
  1376. darkToggle = toggle
  1377. row.addSubview(icon)
  1378. row.addSubview(title)
  1379. row.addSubview(toggle)
  1380. row.onHoverChanged = { hovering in
  1381. row.wantsLayer = true
  1382. row.layer?.cornerRadius = 10
  1383. row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
  1384. }
  1385. row.onHoverChanged?(false)
  1386. NSLayoutConstraint.activate([
  1387. icon.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
  1388. icon.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  1389. title.leadingAnchor.constraint(equalTo: icon.trailingAnchor, constant: 10),
  1390. title.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  1391. toggle.trailingAnchor.constraint(equalTo: row.trailingAnchor, constant: -2),
  1392. toggle.centerYAnchor.constraint(equalTo: row.centerYAnchor)
  1393. ])
  1394. return row
  1395. }
  1396. private func settingsActionRow(icon: String, title: String, action: SettingsAction) -> NSView {
  1397. let row = HoverTrackingView()
  1398. row.translatesAutoresizingMaskIntoConstraints = false
  1399. row.heightAnchor.constraint(equalToConstant: 42).isActive = true
  1400. let iconLabel = NSTextField(labelWithString: icon)
  1401. iconLabel.translatesAutoresizingMaskIntoConstraints = false
  1402. iconLabel.font = NSFont.systemFont(ofSize: 18, weight: .medium)
  1403. iconLabel.textColor = .white
  1404. let titleLabel = NSTextField(labelWithString: title)
  1405. titleLabel.translatesAutoresizingMaskIntoConstraints = false
  1406. titleLabel.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
  1407. titleLabel.textColor = .white
  1408. row.addSubview(iconLabel)
  1409. row.addSubview(titleLabel)
  1410. NSLayoutConstraint.activate([
  1411. iconLabel.leadingAnchor.constraint(equalTo: row.leadingAnchor, constant: 4),
  1412. iconLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor),
  1413. titleLabel.leadingAnchor.constraint(equalTo: iconLabel.trailingAnchor, constant: 10),
  1414. titleLabel.centerYAnchor.constraint(equalTo: row.centerYAnchor)
  1415. ])
  1416. let click = NSClickGestureRecognizer(target: self, action: #selector(settingsActionClicked(_:)))
  1417. row.addGestureRecognizer(click)
  1418. row.identifier = NSUserInterfaceItemIdentifier(rawValue: "\(action.rawValue)")
  1419. row.onHoverChanged = { hovering in
  1420. row.wantsLayer = true
  1421. row.layer?.cornerRadius = 10
  1422. row.layer?.backgroundColor = (hovering ? NSColor(calibratedWhite: 1, alpha: 0.06) : NSColor.clear).cgColor
  1423. }
  1424. row.onHoverChanged?(false)
  1425. return row
  1426. }
  1427. @objc private func darkModeToggled(_ sender: NSSwitch) {
  1428. onToggleDarkMode(sender.state == .on)
  1429. }
  1430. @objc private func settingsActionClicked(_ sender: NSClickGestureRecognizer) {
  1431. guard let view = sender.view,
  1432. let raw = Int(view.identifier?.rawValue ?? ""),
  1433. let action = SettingsAction(rawValue: raw) else { return }
  1434. onAction(action)
  1435. }
  1436. }
  1437. private extension ViewController {
  1438. func roundedContainer(cornerRadius: CGFloat, color: NSColor) -> NSView {
  1439. let view = NSView()
  1440. view.wantsLayer = true
  1441. view.layer?.backgroundColor = color.cgColor
  1442. view.layer?.cornerRadius = cornerRadius
  1443. return view
  1444. }
  1445. func styleSurface(_ view: NSView, borderColor: NSColor, borderWidth: CGFloat, shadow: Bool) {
  1446. view.layer?.borderColor = borderColor.cgColor
  1447. view.layer?.borderWidth = borderWidth
  1448. if shadow {
  1449. view.layer?.shadowColor = NSColor.black.cgColor
  1450. view.layer?.shadowOpacity = 0.18
  1451. view.layer?.shadowOffset = CGSize(width: 0, height: -1)
  1452. view.layer?.shadowRadius = 5
  1453. }
  1454. }
  1455. func textLabel(_ text: String, font: NSFont, color: NSColor) -> NSTextField {
  1456. let label = NSTextField(labelWithString: text)
  1457. label.translatesAutoresizingMaskIntoConstraints = false
  1458. label.textColor = color
  1459. label.font = font
  1460. return label
  1461. }
  1462. func iconLabel(_ text: String, size: CGFloat) -> NSTextField {
  1463. let label = NSTextField(labelWithString: text)
  1464. label.translatesAutoresizingMaskIntoConstraints = false
  1465. label.font = NSFont.systemFont(ofSize: size)
  1466. return label
  1467. }
  1468. func sidebarSectionTitle(_ text: String) -> NSTextField {
  1469. let field = textLabel(text, font: typography.sidebarSection, color: palette.textMuted)
  1470. field.alignment = .left
  1471. return field
  1472. }
  1473. func sidebarItem(_ text: String, icon: String, page: SidebarPage, logoImageName: String? = nil, logoIconWidth: CGFloat = 18, logoHeightMultiplier: CGFloat = 1, logoTemplate: Bool = true, showsDisclosure: Bool = false) -> NSView {
  1474. let item = HoverTrackingView()
  1475. item.wantsLayer = true
  1476. item.layer?.cornerRadius = 10
  1477. item.layer?.backgroundColor = NSColor.clear.cgColor
  1478. item.translatesAutoresizingMaskIntoConstraints = false
  1479. item.heightAnchor.constraint(equalToConstant: 36).isActive = true
  1480. item.layer?.borderWidth = 0
  1481. sidebarPageByView[ObjectIdentifier(item)] = page
  1482. let leadingView: NSView
  1483. if let name = logoImageName, let logo = NSImage(named: name) {
  1484. let imageView = NSImageView(image: logo)
  1485. imageView.translatesAutoresizingMaskIntoConstraints = false
  1486. imageView.imageScaling = .scaleProportionallyDown
  1487. imageView.imageAlignment = .alignCenter
  1488. imageView.isEditable = false
  1489. leadingView = imageView
  1490. } else {
  1491. leadingView = textLabel(icon, font: typography.sidebarIcon, color: palette.textSecondary)
  1492. }
  1493. let titleLabel = textLabel(text, font: typography.sidebarItem, color: palette.textSecondary)
  1494. titleLabel.alignment = .left
  1495. item.addSubview(leadingView)
  1496. item.addSubview(titleLabel)
  1497. var constraints: [NSLayoutConstraint] = [
  1498. leadingView.leadingAnchor.constraint(equalTo: item.leadingAnchor, constant: 12),
  1499. leadingView.centerYAnchor.constraint(equalTo: item.centerYAnchor),
  1500. titleLabel.leadingAnchor.constraint(equalTo: leadingView.trailingAnchor, constant: 8),
  1501. titleLabel.centerYAnchor.constraint(equalTo: item.centerYAnchor)
  1502. ]
  1503. if showsDisclosure {
  1504. let chevron = textLabel("›", font: NSFont.systemFont(ofSize: 22, weight: .semibold), color: palette.textSecondary)
  1505. chevron.translatesAutoresizingMaskIntoConstraints = false
  1506. chevron.alignment = .right
  1507. item.addSubview(chevron)
  1508. constraints.append(contentsOf: [
  1509. chevron.trailingAnchor.constraint(equalTo: item.trailingAnchor, constant: -10),
  1510. chevron.centerYAnchor.constraint(equalTo: item.centerYAnchor),
  1511. titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: chevron.leadingAnchor, constant: -8)
  1512. ])
  1513. } else {
  1514. constraints.append(titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: item.trailingAnchor, constant: -10))
  1515. }
  1516. if logoImageName != nil {
  1517. let h = logoIconWidth * logoHeightMultiplier
  1518. constraints.append(contentsOf: [
  1519. leadingView.widthAnchor.constraint(equalToConstant: logoIconWidth),
  1520. leadingView.heightAnchor.constraint(equalToConstant: h)
  1521. ])
  1522. }
  1523. NSLayoutConstraint.activate(constraints)
  1524. applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate)
  1525. item.onHoverChanged = { [weak self, weak item] hovering in
  1526. guard let self, let item else { return }
  1527. self.applySidebarRowStyle(item, page: page, logoTemplate: logoTemplate, hovering: hovering)
  1528. }
  1529. let click = NSClickGestureRecognizer(target: self, action: #selector(sidebarItemClicked(_:)))
  1530. item.addGestureRecognizer(click)
  1531. return item
  1532. }
  1533. func applySidebarRowStyle(_ item: NSView, page: SidebarPage, logoTemplate: Bool, hovering: Bool = false) {
  1534. let selected = (page == selectedSidebarPage)
  1535. let hoverColor = NSColor(calibratedWhite: 1, alpha: 0.07)
  1536. item.layer?.backgroundColor = (selected ? palette.primaryBlue : (hovering ? hoverColor : NSColor.clear)).cgColor
  1537. let tint = selected ? NSColor.white : palette.textSecondary
  1538. guard item.subviews.count >= 2 else { return }
  1539. let leading = item.subviews[0]
  1540. let title = item.subviews.first { $0 is NSTextField } as? NSTextField
  1541. title?.textColor = tint
  1542. // Optional disclosure chevron (if present) is the last text field.
  1543. if let chevron = item.subviews.last as? NSTextField, chevron !== title {
  1544. chevron.textColor = tint
  1545. }
  1546. if let imageView = leading as? NSImageView {
  1547. if logoTemplate {
  1548. imageView.contentTintColor = tint
  1549. }
  1550. } else if let iconField = leading as? NSTextField {
  1551. iconField.textColor = tint
  1552. }
  1553. }
  1554. func actionButton(title: String, color: NSColor, textColor: NSColor, width: CGFloat) -> NSView {
  1555. let button = HoverTrackingView()
  1556. button.wantsLayer = true
  1557. button.layer?.cornerRadius = 9
  1558. button.layer?.backgroundColor = color.cgColor
  1559. button.translatesAutoresizingMaskIntoConstraints = false
  1560. button.widthAnchor.constraint(equalToConstant: width).isActive = true
  1561. button.heightAnchor.constraint(equalToConstant: 36).isActive = true
  1562. styleSurface(button, borderColor: title == "Cancel" ? palette.inputBorder : palette.primaryBlueBorder, borderWidth: 1, shadow: false)
  1563. if title == "Cancel" {
  1564. button.layer?.backgroundColor = palette.cancelButton.cgColor
  1565. }
  1566. let label = textLabel(title, font: typography.buttonText, color: textColor)
  1567. button.addSubview(label)
  1568. NSLayoutConstraint.activate([
  1569. label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
  1570. label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
  1571. ])
  1572. let baseColor = (title == "Cancel") ? palette.cancelButton : color
  1573. let hoverColor = baseColor.blended(withFraction: 0.12, of: NSColor.white) ?? baseColor
  1574. button.onHoverChanged = { hovering in
  1575. button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  1576. }
  1577. button.onHoverChanged?(false)
  1578. return button
  1579. }
  1580. func iconRoundButton(_ symbol: String, size: CGFloat) -> NSView {
  1581. let button = HoverTrackingView()
  1582. button.wantsLayer = true
  1583. button.layer?.cornerRadius = size / 2
  1584. button.layer?.backgroundColor = palette.inputBackground.cgColor
  1585. button.translatesAutoresizingMaskIntoConstraints = false
  1586. button.widthAnchor.constraint(equalToConstant: size).isActive = true
  1587. button.heightAnchor.constraint(equalToConstant: size).isActive = true
  1588. styleSurface(button, borderColor: palette.inputBorder, borderWidth: 1, shadow: false)
  1589. let label = textLabel(symbol, font: typography.iconButton, color: palette.textSecondary)
  1590. button.addSubview(label)
  1591. NSLayoutConstraint.activate([
  1592. label.centerXAnchor.constraint(equalTo: button.centerXAnchor),
  1593. label.centerYAnchor.constraint(equalTo: button.centerYAnchor)
  1594. ])
  1595. let baseColor = palette.inputBackground
  1596. let hoverColor = baseColor.blended(withFraction: 0.10, of: NSColor.white) ?? baseColor
  1597. button.onHoverChanged = { hovering in
  1598. button.layer?.backgroundColor = (hovering ? hoverColor : baseColor).cgColor
  1599. }
  1600. button.onHoverChanged?(false)
  1601. return button
  1602. }
  1603. }
  1604. private struct Palette {
  1605. let pageBackground = NSColor(calibratedRed: 10.0 / 255.0, green: 11.0 / 255.0, blue: 12.0 / 255.0, alpha: 1)
  1606. let sidebarBackground = NSColor(calibratedRed: 16.0 / 255.0, green: 17.0 / 255.0, blue: 19.0 / 255.0, alpha: 1)
  1607. let sectionCard = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  1608. let tabBarBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  1609. let tabIdleBackground = NSColor(calibratedRed: 22.0 / 255.0, green: 23.0 / 255.0, blue: 26.0 / 255.0, alpha: 1)
  1610. let inputBackground = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
  1611. let inputBorder = NSColor(calibratedRed: 38.0 / 255.0, green: 40.0 / 255.0, blue: 44.0 / 255.0, alpha: 1)
  1612. let primaryBlue = NSColor(calibratedRed: 27.0 / 255.0, green: 115.0 / 255.0, blue: 232.0 / 255.0, alpha: 1)
  1613. let primaryBlueBorder = NSColor(calibratedRed: 42.0 / 255.0, green: 118.0 / 255.0, blue: 220.0 / 255.0, alpha: 1)
  1614. let cancelButton = NSColor(calibratedRed: 20.0 / 255.0, green: 21.0 / 255.0, blue: 24.0 / 255.0, alpha: 1)
  1615. let meetingBadge = NSColor(calibratedRed: 0.88, green: 0.66, blue: 0.14, alpha: 1)
  1616. let separator = NSColor(calibratedRed: 26.0 / 255.0, green: 27.0 / 255.0, blue: 30.0 / 255.0, alpha: 1)
  1617. let textPrimary = NSColor(calibratedWhite: 0.98, alpha: 1)
  1618. let textSecondary = NSColor(calibratedWhite: 0.78, alpha: 1)
  1619. let textTertiary = NSColor(calibratedWhite: 0.66, alpha: 1)
  1620. let textMuted = NSColor(calibratedWhite: 0.44, alpha: 1)
  1621. }
  1622. private struct Typography {
  1623. let sidebarBrand = NSFont.systemFont(ofSize: 26, weight: .bold)
  1624. let sidebarSection = NSFont.systemFont(ofSize: 11, weight: .medium)
  1625. let sidebarIcon = NSFont.systemFont(ofSize: 12, weight: .medium)
  1626. let sidebarItem = NSFont.systemFont(ofSize: 16, weight: .medium)
  1627. let pageTitle = NSFont.systemFont(ofSize: 27, weight: .semibold)
  1628. let joinWithURLTitle = NSFont.systemFont(ofSize: 17, weight: .semibold)
  1629. let sectionTitleBold = NSFont.systemFont(ofSize: 25, weight: .bold)
  1630. let dateHeading = NSFont.systemFont(ofSize: 18, weight: .medium)
  1631. let tabIcon = NSFont.systemFont(ofSize: 13, weight: .regular)
  1632. let tabTitle = NSFont.systemFont(ofSize: 31 / 2, weight: .semibold)
  1633. let fieldLabel = NSFont.systemFont(ofSize: 15, weight: .medium)
  1634. let inputPlaceholder = NSFont.systemFont(ofSize: 14, weight: .regular)
  1635. let buttonText = NSFont.systemFont(ofSize: 16, weight: .medium)
  1636. let filterText = NSFont.systemFont(ofSize: 15, weight: .regular)
  1637. let filterArrow = NSFont.systemFont(ofSize: 12, weight: .regular)
  1638. let iconButton = NSFont.systemFont(ofSize: 14, weight: .medium)
  1639. let cardIcon = NSFont.systemFont(ofSize: 8, weight: .bold)
  1640. let cardTitle = NSFont.systemFont(ofSize: 15, weight: .semibold)
  1641. let cardSubtitle = NSFont.systemFont(ofSize: 13, weight: .bold)
  1642. let cardTime = NSFont.systemFont(ofSize: 12, weight: .regular)
  1643. }