|
|
@@ -592,6 +592,7 @@ final class CVMakerPageView: NSView {
|
|
|
|
|
|
private var appearanceObserver: NSObjectProtocol?
|
|
|
private var languageObserver: NSObjectProtocol?
|
|
|
+ private var scrollBoundsObserver: NSObjectProtocol?
|
|
|
/// AI catalog keyed by `AppLanguage.localeIdentifier` so switching language can restore without a network round-trip.
|
|
|
private var aiCatalogByLocale: [String: [CVTemplate]] = [:]
|
|
|
/// Ignores stale OpenAI responses after a newer fetch or language change started.
|
|
|
@@ -686,6 +687,9 @@ final class CVMakerPageView: NSView {
|
|
|
if let languageObserver {
|
|
|
NotificationCenter.default.removeObserver(languageObserver)
|
|
|
}
|
|
|
+ if let scrollBoundsObserver {
|
|
|
+ NotificationCenter.default.removeObserver(scrollBoundsObserver)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
@available(*, unavailable)
|
|
|
@@ -828,6 +832,14 @@ final class CVMakerPageView: NSView {
|
|
|
scrollView.drawsBackground = false
|
|
|
scrollView.borderType = .noBorder
|
|
|
scrollView.documentView = gridDocument
|
|
|
+ scrollView.contentView.postsBoundsChangedNotifications = true
|
|
|
+ scrollBoundsObserver = NotificationCenter.default.addObserver(
|
|
|
+ forName: NSView.boundsDidChangeNotification,
|
|
|
+ object: scrollView.contentView,
|
|
|
+ queue: .main
|
|
|
+ ) { [weak self] _ in
|
|
|
+ self?.resetTemplateCardHoverStates()
|
|
|
+ }
|
|
|
NSLayoutConstraint.activate([
|
|
|
gridDocument.topAnchor.constraint(equalTo: scrollView.contentView.topAnchor),
|
|
|
gridDocument.leadingAnchor.constraint(equalTo: scrollView.contentView.leadingAnchor),
|
|
|
@@ -980,9 +992,10 @@ final class CVMakerPageView: NSView {
|
|
|
let template = templates[position]
|
|
|
let card = CVTemplateCard(template: template, palette: palette())
|
|
|
card.translatesAutoresizingMaskIntoConstraints = false
|
|
|
+ let selectionToken = card.selectionToken
|
|
|
+ let templateID = card.templateID
|
|
|
card.onSelect = { [weak self] in
|
|
|
- guard let self else { return }
|
|
|
- self.didSelectCard(card)
|
|
|
+ self?.didSelectCard(selectionToken: selectionToken, templateID: templateID)
|
|
|
}
|
|
|
row.addArrangedSubview(card)
|
|
|
templateCardsInGrid.append(card)
|
|
|
@@ -1036,7 +1049,18 @@ final class CVMakerPageView: NSView {
|
|
|
private func applySelectionToCards() {
|
|
|
let token = selectedTemplateCardToken
|
|
|
for card in templateCardsInGrid {
|
|
|
- card.isSelected = (card.selectionToken == token)
|
|
|
+ let shouldSelect = (card.selectionToken == token)
|
|
|
+ card.isSelected = shouldSelect
|
|
|
+ if !shouldSelect {
|
|
|
+ card.clearHoverVisualState()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// Scroll moves cards under a stationary cursor without `mouseExited`; clear stale hovers so only the selected card keeps a brand border.
|
|
|
+ private func resetTemplateCardHoverStates() {
|
|
|
+ for card in templateCardsInGrid where !card.isSelected {
|
|
|
+ card.clearHoverVisualState()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -1078,9 +1102,9 @@ final class CVMakerPageView: NSView {
|
|
|
updateSelectedChipStates()
|
|
|
}
|
|
|
|
|
|
- private func didSelectCard(_ card: CVTemplateCard) {
|
|
|
- selectedTemplateCardToken = card.selectionToken
|
|
|
- selectedTemplateID = card.templateID
|
|
|
+ private func didSelectCard(selectionToken: UUID, templateID: String) {
|
|
|
+ selectedTemplateCardToken = selectionToken
|
|
|
+ selectedTemplateID = templateID
|
|
|
applySelectionToCards()
|
|
|
}
|
|
|
|
|
|
@@ -1602,6 +1626,17 @@ private final class CVTemplateCard: NSView {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+ /// Clears hover chrome without waiting for `mouseExited` (e.g. after scroll or when another card is selected).
|
|
|
+ func clearHoverVisualState() {
|
|
|
+ if didPushCursor {
|
|
|
+ NSCursor.pop()
|
|
|
+ didPushCursor = false
|
|
|
+ }
|
|
|
+ guard isHovering else { return }
|
|
|
+ isHovering = false
|
|
|
+ applyChrome()
|
|
|
+ }
|
|
|
+
|
|
|
private func applyChrome() {
|
|
|
// Same border + shadow metrics in every state — only color changes on select
|
|
|
// so thumbnails keep identical insets (stroke is drawn inside the layer).
|