Bläddra i källkod

Improve create meeting popover clarity and notes field interactions.

Redesign the panel styling for stronger contrast, keep labels anchored left, and add notes hover/focus states with immediate caret focus behavior.

Made-with: Cursor
huzaifahayat12 1 vecka sedan
förälder
incheckning
7627652e2c
1 ändrade filer med 183 tillägg och 23 borttagningar
  1. 183 23
      meetings_app/ViewController.swift

+ 183 - 23
meetings_app/ViewController.swift

@@ -4857,7 +4857,7 @@ private final class CalendarDayActionMenuViewController: NSViewController {
4857 4857
     }
4858 4858
 }
4859 4859
 
4860
-private final class CreateMeetingPopoverViewController: NSViewController {
4860
+private final class CreateMeetingPopoverViewController: NSViewController, NSTextViewDelegate {
4861 4861
     struct Draft {
4862 4862
         let title: String
4863 4863
         let notes: String?
@@ -4875,7 +4875,13 @@ private final class CreateMeetingPopoverViewController: NSViewController {
4875 4875
     private var timePicker: NSDatePicker?
4876 4876
     private var durationField: NSTextField?
4877 4877
     private var notesView: NSTextView?
4878
+    private var notesScrollView: NSScrollView?
4878 4879
     private var errorLabel: NSTextField?
4880
+    private var notesBorderIdle = NSColor.clear
4881
+    private var notesBorderHover = NSColor.clear
4882
+    private var notesBorderFocused = NSColor.clear
4883
+    private var notesIsHovered = false
4884
+    private var notesIsFocused = false
4879 4885
 
4880 4886
     init(palette: Palette,
4881 4887
          typography: Typography,
@@ -4896,16 +4902,28 @@ private final class CreateMeetingPopoverViewController: NSViewController {
4896 4902
         let root = NSView()
4897 4903
         root.translatesAutoresizingMaskIntoConstraints = false
4898 4904
         root.userInterfaceLayoutDirection = .leftToRight
4905
+        root.wantsLayer = true
4906
+        root.layer?.cornerRadius = 14
4907
+        root.layer?.masksToBounds = true
4908
+        root.layer?.backgroundColor = palette.sectionCard.cgColor
4909
+        root.layer?.borderWidth = 1
4910
+        root.layer?.borderColor = palette.inputBorder.withAlphaComponent(0.9).cgColor
4899 4911
 
4900 4912
         let stack = NSStackView()
4901 4913
         stack.translatesAutoresizingMaskIntoConstraints = false
4902 4914
         stack.orientation = .vertical
4903 4915
         stack.alignment = .leading
4904
-        stack.spacing = 12
4916
+        stack.spacing = 14
4905 4917
         stack.userInterfaceLayoutDirection = .leftToRight
4906 4918
 
4919
+        let inputSurface = palette.inputBackground.blended(withFraction: 0.18, of: palette.sectionCard) ?? palette.inputBackground
4920
+        let fieldBorder = palette.textSecondary.withAlphaComponent(0.4)
4921
+        notesBorderIdle = fieldBorder
4922
+        notesBorderHover = palette.textSecondary.withAlphaComponent(0.72)
4923
+        notesBorderFocused = palette.primaryBlueBorder
4924
+
4907 4925
         let header = NSTextField(labelWithString: "Schedule meeting")
4908
-        header.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
4926
+        header.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
4909 4927
         header.textColor = palette.textPrimary
4910 4928
         header.alignment = .left
4911 4929
         header.userInterfaceLayoutDirection = .leftToRight
@@ -4922,9 +4940,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
4922 4940
         titleShell.translatesAutoresizingMaskIntoConstraints = false
4923 4941
         titleShell.wantsLayer = true
4924 4942
         titleShell.layer?.cornerRadius = 8
4925
-        titleShell.layer?.backgroundColor = palette.inputBackground.cgColor
4926
-        titleShell.layer?.borderColor = palette.inputBorder.cgColor
4927
-        titleShell.layer?.borderWidth = 1
4943
+        titleShell.layer?.backgroundColor = inputSurface.cgColor
4944
+        titleShell.layer?.borderColor = fieldBorder.cgColor
4945
+        titleShell.layer?.borderWidth = 1.2
4928 4946
         titleShell.heightAnchor.constraint(equalToConstant: 40).isActive = true
4929 4947
 
4930 4948
         let titleField = NSTextField(string: "")
@@ -4961,9 +4979,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
4961 4979
         pickerShell.translatesAutoresizingMaskIntoConstraints = false
4962 4980
         pickerShell.wantsLayer = true
4963 4981
         pickerShell.layer?.cornerRadius = 8
4964
-        pickerShell.layer?.backgroundColor = palette.inputBackground.cgColor
4965
-        pickerShell.layer?.borderColor = palette.inputBorder.cgColor
4966
-        pickerShell.layer?.borderWidth = 1
4982
+        pickerShell.layer?.backgroundColor = inputSurface.cgColor
4983
+        pickerShell.layer?.borderColor = fieldBorder.cgColor
4984
+        pickerShell.layer?.borderWidth = 1.2
4967 4985
         pickerShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
4968 4986
 
4969 4987
         let timePicker = NSDatePicker()
@@ -4995,9 +5013,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
4995 5013
         durationShell.translatesAutoresizingMaskIntoConstraints = false
4996 5014
         durationShell.wantsLayer = true
4997 5015
         durationShell.layer?.cornerRadius = 8
4998
-        durationShell.layer?.backgroundColor = palette.inputBackground.cgColor
4999
-        durationShell.layer?.borderColor = palette.inputBorder.cgColor
5000
-        durationShell.layer?.borderWidth = 1
5016
+        durationShell.layer?.backgroundColor = inputSurface.cgColor
5017
+        durationShell.layer?.borderColor = fieldBorder.cgColor
5018
+        durationShell.layer?.borderWidth = 1.2
5001 5019
         durationShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
5002 5020
 
5003 5021
         let durationField = NSTextField(string: "30")
@@ -5039,25 +5057,54 @@ private final class CreateMeetingPopoverViewController: NSViewController {
5039 5057
         notesLabel.userInterfaceLayoutDirection = .leftToRight
5040 5058
         notesLabel.baseWritingDirection = .leftToRight
5041 5059
 
5042
-        let notesScroll = NSScrollView()
5060
+        let notesScroll = HoverFocusScrollView()
5043 5061
         notesScroll.translatesAutoresizingMaskIntoConstraints = false
5044 5062
         notesScroll.drawsBackground = true
5045
-        notesScroll.backgroundColor = palette.inputBackground
5063
+        notesScroll.backgroundColor = inputSurface
5046 5064
         notesScroll.hasVerticalScroller = true
5065
+        notesScroll.hasHorizontalScroller = false
5047 5066
         notesScroll.borderType = .noBorder
5048 5067
         notesScroll.wantsLayer = true
5049 5068
         notesScroll.layer?.cornerRadius = 8
5050
-        notesScroll.layer?.borderWidth = 1
5051
-        notesScroll.layer?.borderColor = palette.inputBorder.cgColor
5052
-        notesScroll.heightAnchor.constraint(equalToConstant: 90).isActive = true
5069
+        notesScroll.layer?.masksToBounds = true
5070
+        notesScroll.layer?.borderWidth = 1.2
5071
+        notesScroll.layer?.borderColor = notesBorderIdle.cgColor
5072
+        notesScroll.heightAnchor.constraint(equalToConstant: 100).isActive = true
5073
+        notesScroll.onHoverChanged = { [weak self] hovering in
5074
+            guard let self else { return }
5075
+            self.notesIsHovered = hovering
5076
+            self.updateNotesBorderAppearance()
5077
+        }
5078
+        notesScroll.onMouseDown = { [weak self] in
5079
+            guard let self, let notesView = self.notesView as? ImmediateFocusTextView else { return }
5080
+            self.view.window?.makeFirstResponder(notesView)
5081
+            notesView.ensureCaretVisibleImmediately()
5082
+            self.notesIsFocused = true
5083
+            self.updateNotesBorderAppearance()
5084
+        }
5053 5085
 
5054
-        let notesView = NSTextView()
5086
+        let notesView = ImmediateFocusTextView(frame: .zero)
5055 5087
         notesView.drawsBackground = false
5056 5088
         notesView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
5057 5089
         notesView.textColor = palette.textPrimary
5058 5090
         notesView.insertionPointColor = palette.textPrimary
5091
+        notesView.isEditable = true
5092
+        notesView.isSelectable = true
5093
+        notesView.isRichText = false
5094
+        notesView.importsGraphics = false
5095
+        notesView.isHorizontallyResizable = false
5096
+        notesView.isVerticallyResizable = true
5097
+        notesView.autoresizingMask = [.width]
5098
+        notesView.textContainerInset = NSSize(width: 6, height: 6)
5099
+        notesView.textContainer?.widthTracksTextView = true
5100
+        notesView.textContainer?.containerSize = NSSize(
5101
+            width: notesScroll.contentSize.width,
5102
+            height: CGFloat.greatestFiniteMagnitude
5103
+        )
5104
+        notesView.delegate = self
5059 5105
         notesScroll.documentView = notesView
5060 5106
         self.notesView = notesView
5107
+        self.notesScrollView = notesScroll
5061 5108
 
5062 5109
         let error = NSTextField(labelWithString: "")
5063 5110
         error.translatesAutoresizingMaskIntoConstraints = false
@@ -5098,11 +5145,11 @@ private final class CreateMeetingPopoverViewController: NSViewController {
5098 5145
 
5099 5146
         root.addSubview(stack)
5100 5147
         NSLayoutConstraint.activate([
5101
-            root.widthAnchor.constraint(equalToConstant: 360),
5102
-            stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 14),
5103
-            stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -14),
5104
-            stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 12),
5105
-            stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -12),
5148
+            root.widthAnchor.constraint(equalToConstant: 372),
5149
+            stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
5150
+            stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
5151
+            stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 16),
5152
+            stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16),
5106 5153
             titleShell.widthAnchor.constraint(equalTo: stack.widthAnchor),
5107 5154
             timeRow.widthAnchor.constraint(equalTo: stack.widthAnchor),
5108 5155
             notesScroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
@@ -5154,11 +5201,124 @@ private final class CreateMeetingPopoverViewController: NSViewController {
5154 5201
         onSave(Draft(title: title, notes: cleanedNotes, startDate: start, endDate: end))
5155 5202
     }
5156 5203
 
5204
+    private func updateNotesBorderAppearance() {
5205
+        let color: NSColor
5206
+        let width: CGFloat
5207
+        if notesIsFocused {
5208
+            color = notesBorderFocused
5209
+            width = 1.6
5210
+        } else if notesIsHovered {
5211
+            color = notesBorderHover
5212
+            width = 1.4
5213
+        } else {
5214
+            color = notesBorderIdle
5215
+            width = 1.2
5216
+        }
5217
+        notesScrollView?.layer?.borderColor = color.cgColor
5218
+        notesScrollView?.layer?.borderWidth = width
5219
+    }
5220
+
5157 5221
     private func setError(_ message: String?) {
5158 5222
         guard let errorLabel else { return }
5159 5223
         errorLabel.stringValue = message ?? ""
5160 5224
         errorLabel.isHidden = message == nil
5161 5225
     }
5226
+
5227
+    func textDidBeginEditing(_ notification: Notification) {
5228
+        guard let current = notification.object as? NSTextView, current === notesView else { return }
5229
+        notesIsFocused = true
5230
+        updateNotesBorderAppearance()
5231
+    }
5232
+
5233
+    func textDidEndEditing(_ notification: Notification) {
5234
+        guard let current = notification.object as? NSTextView, current === notesView else { return }
5235
+        notesIsFocused = false
5236
+        updateNotesBorderAppearance()
5237
+    }
5238
+}
5239
+
5240
+private final class HoverFocusScrollView: NSScrollView {
5241
+    var onHoverChanged: ((Bool) -> Void)?
5242
+    var onMouseDown: (() -> Void)?
5243
+    private var hoverTrackingArea: NSTrackingArea?
5244
+
5245
+    override func updateTrackingAreas() {
5246
+        super.updateTrackingAreas()
5247
+        if let hoverTrackingArea {
5248
+            removeTrackingArea(hoverTrackingArea)
5249
+        }
5250
+        let area = NSTrackingArea(
5251
+            rect: bounds,
5252
+            options: [.activeInActiveApp, .inVisibleRect, .mouseEnteredAndExited],
5253
+            owner: self,
5254
+            userInfo: nil
5255
+        )
5256
+        addTrackingArea(area)
5257
+        hoverTrackingArea = area
5258
+    }
5259
+
5260
+    override func mouseEntered(with event: NSEvent) {
5261
+        super.mouseEntered(with: event)
5262
+        onHoverChanged?(true)
5263
+    }
5264
+
5265
+    override func mouseExited(with event: NSEvent) {
5266
+        super.mouseExited(with: event)
5267
+        onHoverChanged?(false)
5268
+    }
5269
+
5270
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
5271
+        true
5272
+    }
5273
+
5274
+    override func mouseDown(with event: NSEvent) {
5275
+        onMouseDown?()
5276
+        if let window, !window.isKeyWindow {
5277
+            window.makeKeyAndOrderFront(nil)
5278
+            return
5279
+        }
5280
+        // Forward the click straight to the text view so caret placement/blink starts immediately.
5281
+        if let textView = documentView as? NSTextView {
5282
+            window?.makeFirstResponder(textView)
5283
+            textView.mouseDown(with: event)
5284
+            return
5285
+        }
5286
+        super.mouseDown(with: event)
5287
+    }
5288
+}
5289
+
5290
+private final class ImmediateFocusTextView: NSTextView {
5291
+    override var acceptsFirstResponder: Bool { true }
5292
+
5293
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
5294
+        true
5295
+    }
5296
+
5297
+    @discardableResult
5298
+    override func becomeFirstResponder() -> Bool {
5299
+        let accepted = super.becomeFirstResponder()
5300
+        if accepted {
5301
+            ensureCaretVisibleImmediately()
5302
+        }
5303
+        return accepted
5304
+    }
5305
+
5306
+    override func mouseDown(with event: NSEvent) {
5307
+        window?.makeFirstResponder(self)
5308
+        super.mouseDown(with: event)
5309
+        ensureCaretVisibleImmediately()
5310
+    }
5311
+
5312
+    func ensureCaretVisibleImmediately() {
5313
+        var range = selectedRange()
5314
+        if range.location == NSNotFound {
5315
+            range = NSRange(location: string.utf16.count, length: 0)
5316
+        }
5317
+        setSelectedRange(range)
5318
+        scrollRangeToVisible(range)
5319
+        needsDisplay = true
5320
+        displayIfNeeded()
5321
+    }
5162 5322
 }
5163 5323
 
5164 5324
 // MARK: - Schedule actions (OAuth entry)