Forráskód Böngészése

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 3 hónapja
szülő
commit
7627652e2c
1 módosított fájl, 183 hozzáadás és 23 törlés
  1. 183 23
      meetings_app/ViewController.swift

+ 183 - 23
meetings_app/ViewController.swift

@@ -4857,7 +4857,7 @@ private final class CalendarDayActionMenuViewController: NSViewController {
     }
 }
 
-private final class CreateMeetingPopoverViewController: NSViewController {
+private final class CreateMeetingPopoverViewController: NSViewController, NSTextViewDelegate {
     struct Draft {
         let title: String
         let notes: String?
@@ -4875,7 +4875,13 @@ private final class CreateMeetingPopoverViewController: NSViewController {
     private var timePicker: NSDatePicker?
     private var durationField: NSTextField?
     private var notesView: NSTextView?
+    private var notesScrollView: NSScrollView?
     private var errorLabel: NSTextField?
+    private var notesBorderIdle = NSColor.clear
+    private var notesBorderHover = NSColor.clear
+    private var notesBorderFocused = NSColor.clear
+    private var notesIsHovered = false
+    private var notesIsFocused = false
 
     init(palette: Palette,
          typography: Typography,
@@ -4896,16 +4902,28 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         let root = NSView()
         root.translatesAutoresizingMaskIntoConstraints = false
         root.userInterfaceLayoutDirection = .leftToRight
+        root.wantsLayer = true
+        root.layer?.cornerRadius = 14
+        root.layer?.masksToBounds = true
+        root.layer?.backgroundColor = palette.sectionCard.cgColor
+        root.layer?.borderWidth = 1
+        root.layer?.borderColor = palette.inputBorder.withAlphaComponent(0.9).cgColor
 
         let stack = NSStackView()
         stack.translatesAutoresizingMaskIntoConstraints = false
         stack.orientation = .vertical
         stack.alignment = .leading
-        stack.spacing = 12
+        stack.spacing = 14
         stack.userInterfaceLayoutDirection = .leftToRight
 
+        let inputSurface = palette.inputBackground.blended(withFraction: 0.18, of: palette.sectionCard) ?? palette.inputBackground
+        let fieldBorder = palette.textSecondary.withAlphaComponent(0.4)
+        notesBorderIdle = fieldBorder
+        notesBorderHover = palette.textSecondary.withAlphaComponent(0.72)
+        notesBorderFocused = palette.primaryBlueBorder
+
         let header = NSTextField(labelWithString: "Schedule meeting")
-        header.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
+        header.font = NSFont.systemFont(ofSize: 16, weight: .semibold)
         header.textColor = palette.textPrimary
         header.alignment = .left
         header.userInterfaceLayoutDirection = .leftToRight
@@ -4922,9 +4940,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         titleShell.translatesAutoresizingMaskIntoConstraints = false
         titleShell.wantsLayer = true
         titleShell.layer?.cornerRadius = 8
-        titleShell.layer?.backgroundColor = palette.inputBackground.cgColor
-        titleShell.layer?.borderColor = palette.inputBorder.cgColor
-        titleShell.layer?.borderWidth = 1
+        titleShell.layer?.backgroundColor = inputSurface.cgColor
+        titleShell.layer?.borderColor = fieldBorder.cgColor
+        titleShell.layer?.borderWidth = 1.2
         titleShell.heightAnchor.constraint(equalToConstant: 40).isActive = true
 
         let titleField = NSTextField(string: "")
@@ -4961,9 +4979,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         pickerShell.translatesAutoresizingMaskIntoConstraints = false
         pickerShell.wantsLayer = true
         pickerShell.layer?.cornerRadius = 8
-        pickerShell.layer?.backgroundColor = palette.inputBackground.cgColor
-        pickerShell.layer?.borderColor = palette.inputBorder.cgColor
-        pickerShell.layer?.borderWidth = 1
+        pickerShell.layer?.backgroundColor = inputSurface.cgColor
+        pickerShell.layer?.borderColor = fieldBorder.cgColor
+        pickerShell.layer?.borderWidth = 1.2
         pickerShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
 
         let timePicker = NSDatePicker()
@@ -4995,9 +5013,9 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         durationShell.translatesAutoresizingMaskIntoConstraints = false
         durationShell.wantsLayer = true
         durationShell.layer?.cornerRadius = 8
-        durationShell.layer?.backgroundColor = palette.inputBackground.cgColor
-        durationShell.layer?.borderColor = palette.inputBorder.cgColor
-        durationShell.layer?.borderWidth = 1
+        durationShell.layer?.backgroundColor = inputSurface.cgColor
+        durationShell.layer?.borderColor = fieldBorder.cgColor
+        durationShell.layer?.borderWidth = 1.2
         durationShell.heightAnchor.constraint(equalToConstant: 34).isActive = true
 
         let durationField = NSTextField(string: "30")
@@ -5039,25 +5057,54 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         notesLabel.userInterfaceLayoutDirection = .leftToRight
         notesLabel.baseWritingDirection = .leftToRight
 
-        let notesScroll = NSScrollView()
+        let notesScroll = HoverFocusScrollView()
         notesScroll.translatesAutoresizingMaskIntoConstraints = false
         notesScroll.drawsBackground = true
-        notesScroll.backgroundColor = palette.inputBackground
+        notesScroll.backgroundColor = inputSurface
         notesScroll.hasVerticalScroller = true
+        notesScroll.hasHorizontalScroller = false
         notesScroll.borderType = .noBorder
         notesScroll.wantsLayer = true
         notesScroll.layer?.cornerRadius = 8
-        notesScroll.layer?.borderWidth = 1
-        notesScroll.layer?.borderColor = palette.inputBorder.cgColor
-        notesScroll.heightAnchor.constraint(equalToConstant: 90).isActive = true
+        notesScroll.layer?.masksToBounds = true
+        notesScroll.layer?.borderWidth = 1.2
+        notesScroll.layer?.borderColor = notesBorderIdle.cgColor
+        notesScroll.heightAnchor.constraint(equalToConstant: 100).isActive = true
+        notesScroll.onHoverChanged = { [weak self] hovering in
+            guard let self else { return }
+            self.notesIsHovered = hovering
+            self.updateNotesBorderAppearance()
+        }
+        notesScroll.onMouseDown = { [weak self] in
+            guard let self, let notesView = self.notesView as? ImmediateFocusTextView else { return }
+            self.view.window?.makeFirstResponder(notesView)
+            notesView.ensureCaretVisibleImmediately()
+            self.notesIsFocused = true
+            self.updateNotesBorderAppearance()
+        }
 
-        let notesView = NSTextView()
+        let notesView = ImmediateFocusTextView(frame: .zero)
         notesView.drawsBackground = false
         notesView.font = NSFont.systemFont(ofSize: 13, weight: .regular)
         notesView.textColor = palette.textPrimary
         notesView.insertionPointColor = palette.textPrimary
+        notesView.isEditable = true
+        notesView.isSelectable = true
+        notesView.isRichText = false
+        notesView.importsGraphics = false
+        notesView.isHorizontallyResizable = false
+        notesView.isVerticallyResizable = true
+        notesView.autoresizingMask = [.width]
+        notesView.textContainerInset = NSSize(width: 6, height: 6)
+        notesView.textContainer?.widthTracksTextView = true
+        notesView.textContainer?.containerSize = NSSize(
+            width: notesScroll.contentSize.width,
+            height: CGFloat.greatestFiniteMagnitude
+        )
+        notesView.delegate = self
         notesScroll.documentView = notesView
         self.notesView = notesView
+        self.notesScrollView = notesScroll
 
         let error = NSTextField(labelWithString: "")
         error.translatesAutoresizingMaskIntoConstraints = false
@@ -5098,11 +5145,11 @@ private final class CreateMeetingPopoverViewController: NSViewController {
 
         root.addSubview(stack)
         NSLayoutConstraint.activate([
-            root.widthAnchor.constraint(equalToConstant: 360),
-            stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 14),
-            stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -14),
-            stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 12),
-            stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -12),
+            root.widthAnchor.constraint(equalToConstant: 372),
+            stack.leadingAnchor.constraint(equalTo: root.leadingAnchor, constant: 16),
+            stack.trailingAnchor.constraint(equalTo: root.trailingAnchor, constant: -16),
+            stack.topAnchor.constraint(equalTo: root.topAnchor, constant: 16),
+            stack.bottomAnchor.constraint(equalTo: root.bottomAnchor, constant: -16),
             titleShell.widthAnchor.constraint(equalTo: stack.widthAnchor),
             timeRow.widthAnchor.constraint(equalTo: stack.widthAnchor),
             notesScroll.widthAnchor.constraint(equalTo: stack.widthAnchor),
@@ -5154,11 +5201,124 @@ private final class CreateMeetingPopoverViewController: NSViewController {
         onSave(Draft(title: title, notes: cleanedNotes, startDate: start, endDate: end))
     }
 
+    private func updateNotesBorderAppearance() {
+        let color: NSColor
+        let width: CGFloat
+        if notesIsFocused {
+            color = notesBorderFocused
+            width = 1.6
+        } else if notesIsHovered {
+            color = notesBorderHover
+            width = 1.4
+        } else {
+            color = notesBorderIdle
+            width = 1.2
+        }
+        notesScrollView?.layer?.borderColor = color.cgColor
+        notesScrollView?.layer?.borderWidth = width
+    }
+
     private func setError(_ message: String?) {
         guard let errorLabel else { return }
         errorLabel.stringValue = message ?? ""
         errorLabel.isHidden = message == nil
     }
+
+    func textDidBeginEditing(_ notification: Notification) {
+        guard let current = notification.object as? NSTextView, current === notesView else { return }
+        notesIsFocused = true
+        updateNotesBorderAppearance()
+    }
+
+    func textDidEndEditing(_ notification: Notification) {
+        guard let current = notification.object as? NSTextView, current === notesView else { return }
+        notesIsFocused = false
+        updateNotesBorderAppearance()
+    }
+}
+
+private final class HoverFocusScrollView: NSScrollView {
+    var onHoverChanged: ((Bool) -> Void)?
+    var onMouseDown: (() -> Void)?
+    private var hoverTrackingArea: NSTrackingArea?
+
+    override func updateTrackingAreas() {
+        super.updateTrackingAreas()
+        if let hoverTrackingArea {
+            removeTrackingArea(hoverTrackingArea)
+        }
+        let area = NSTrackingArea(
+            rect: bounds,
+            options: [.activeInActiveApp, .inVisibleRect, .mouseEnteredAndExited],
+            owner: self,
+            userInfo: nil
+        )
+        addTrackingArea(area)
+        hoverTrackingArea = area
+    }
+
+    override func mouseEntered(with event: NSEvent) {
+        super.mouseEntered(with: event)
+        onHoverChanged?(true)
+    }
+
+    override func mouseExited(with event: NSEvent) {
+        super.mouseExited(with: event)
+        onHoverChanged?(false)
+    }
+
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
+    }
+
+    override func mouseDown(with event: NSEvent) {
+        onMouseDown?()
+        if let window, !window.isKeyWindow {
+            window.makeKeyAndOrderFront(nil)
+            return
+        }
+        // Forward the click straight to the text view so caret placement/blink starts immediately.
+        if let textView = documentView as? NSTextView {
+            window?.makeFirstResponder(textView)
+            textView.mouseDown(with: event)
+            return
+        }
+        super.mouseDown(with: event)
+    }
+}
+
+private final class ImmediateFocusTextView: NSTextView {
+    override var acceptsFirstResponder: Bool { true }
+
+    override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
+        true
+    }
+
+    @discardableResult
+    override func becomeFirstResponder() -> Bool {
+        let accepted = super.becomeFirstResponder()
+        if accepted {
+            ensureCaretVisibleImmediately()
+        }
+        return accepted
+    }
+
+    override func mouseDown(with event: NSEvent) {
+        window?.makeFirstResponder(self)
+        super.mouseDown(with: event)
+        ensureCaretVisibleImmediately()
+    }
+
+    func ensureCaretVisibleImmediately() {
+        var range = selectedRange()
+        if range.location == NSNotFound {
+            range = NSRange(location: string.utf16.count, length: 0)
+        }
+        setSelectedRange(range)
+        scrollRangeToVisible(range)
+        needsDisplay = true
+        displayIfNeeded()
+    }
 }
 
 // MARK: - Schedule actions (OAuth entry)