From a255131e66318bed23018f3d6798ebb1ea33676a Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Thu, 25 Jun 2026 13:50:57 +0800
Subject: [PATCH] Freeze 8.0.2 usage tips banner
---
src/Apptag/Localization/fr.json | 5
src/Apptag/Localization/ru.json | 5
src/Apptag/AppGridCollectionView.swift | 417 +++++++++++++++++---
src/Scripts/usage_tips_qa.sh | 539 +++++++++++---------------
src/Apptag/Localization/ar.json | 5
src/Apptag/Localization/ro.json | 5
src/Apptag/Localization/en.json | 5
src/Apptag/Localization/sv.json | 5
src/Apptag/Localization/zh-Hant.json | 5
src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md | 6
src/Apptag/Localization/pl.json | 5
src/Apptag/Localization/es.json | 5
src/Apptag/Localization/th.json | 5
src/Apptag/Localization/nn.json | 5
src/CHANGELOG.md | 11
src/Apptag/Info.plist | 4
src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md | 61 +++
src/Apptag/Localization/cs.json | 5
src/Apptag/Localization/ms.json | 5
src/Apptag/Localization/it.json | 5
src/Apptag/Localization/tr.json | 5
src/Apptag/Localization/da.json | 5
src/Apptag/Localization/id.json | 5
src/Apptag/Localization/ja.json | 5
src/Apptag/Localization/de.json | 5
src/Apptag/Localization/pt-BR.json | 5
CODEGRAPH.md | 8
src/Apptag/Localization/ar-Najdi.json | 5
src/Apptag/Localization/nb.json | 5
src/Apptag/Localization/ko.json | 5
src/Apptag/Localization/no.json | 5
src/Apptag/ContentView.swift | 1
src/Apptag/Localization/sr-Cyrl.json | 5
src/Apptag/Localization/zh-Hans.json | 5
src/Apptag/Localization/nl.json | 5
src/Apptag/Localization/uk.json | 5
src/Apptag/Localization/vi.json | 5
37 files changed, 741 insertions(+), 451 deletions(-)
diff --git a/CODEGRAPH.md b/CODEGRAPH.md
index d4f8e62..51602b4 100644
--- a/CODEGRAPH.md
+++ b/CODEGRAPH.md
@@ -13,7 +13,7 @@
- Keeps the most recent in-process `AppLibrarySnapshot` so newly-created overlay views can render immediately while background refresh catches up.
- AppGrid collection renderer: `src/Apptag/AppGridCollectionView.swift`
- - AppKit `NSCollectionView` implementation for grouped app layout, drag/drop, app ordering, bubbles, and usage tips overlay.
+ - AppKit `NSCollectionView` implementation for grouped app layout, drag/drop, app ordering, bubbles, and the native split teaching-banner usage tips overlay.
- Should not own full-screen AppGrid theme gradients; it receives theme only for derived glass readability.
- AppGrid theme model: `src/Apptag/AppGridTheme.swift`
@@ -44,7 +44,7 @@
- `src/Scripts/macos14_build_metadata_qa.sh`
- Verifies build metadata and deployment target.
- `src/Scripts/usage_tips_qa.sh`
- - Verifies usage tips overlay and 29-language coverage.
+ - Verifies native usage tips overlay, split teaching-banner layout, theme-aware accent/readability, click-through protection, close action, ordered-list details, and 29-language coverage.
- `src/Scripts/tag_navigation_hover_scroll_qa.sh`
- Verifies tag hover scroll semantics remain guarded.
- `src/Scripts/tag_double_click_preferences_qa.sh`
@@ -61,5 +61,7 @@
- The default theme preserves the original light AppGrid background.
- Legacy users with `useDarkAppGrid=true` must land on the `deepBlue` theme.
- Tag navigation single-click must keep immediate scroll behavior. Hover must keep guarded auto-scroll. Long-press must keep tag reorder behavior. Double-click may open Preferences on the Tags tab but must not replace those existing behaviors.
+- Usage tips must remain a native AppKit overlay, not SwiftUI. The bottom teaching banner must consume its own click region so it never opens underlying apps; only arrow hit regions change pages.
+- Usage tips title/detail text must remain readable across the default, dark, and bright App Grid themes; all visible usage-tip copy must stay complete across 29 localization files.
-Last updated: 2026-06-24, tag double-click opens Tags settings.
+Last updated: 2026-06-25, usage tips teaching banner visual optimization.
diff --git a/src/Apptag/AppGridCollectionView.swift b/src/Apptag/AppGridCollectionView.swift
index 5d8d845..13fe190 100644
--- a/src/Apptag/AppGridCollectionView.swift
+++ b/src/Apptag/AppGridCollectionView.swift
@@ -38,8 +38,8 @@
}
enum AppGridUsageTipsMetrics {
- static let barHeight: CGFloat = 136
- static let reservedHeight: CGFloat = 176
+ static let barHeight: CGFloat = 154
+ static let reservedHeight: CGFloat = 194
static let bottomMargin: CGFloat = 20
static let horizontalInset: CGFloat = 24
static let minWidth: CGFloat = 640
@@ -71,6 +71,7 @@
let onGroupActivate: (String) -> Void
let onScrollActivity: () -> Void
let onDragModeChange: (Bool) -> Void
+ let onHideUsageTips: () -> Void
let onUsageTipsHoverChange: (Bool) -> Void
func makeCoordinator() -> Coordinator {
@@ -111,6 +112,7 @@
onGroupActivate: onGroupActivate,
onScrollActivity: onScrollActivity,
onDragModeChange: onDragModeChange,
+ onHideUsageTips: onHideUsageTips,
onUsageTipIndexChange: { selectedUsageTipIndexBinding.wrappedValue = $0 },
onUsageTipsHoverChange: onUsageTipsHoverChange
)
@@ -149,6 +151,7 @@
var onGroupActivate: (String) -> Void = { _ in }
var onScrollActivity: () -> Void = {}
var onDragModeChange: (Bool) -> Void = { _ in }
+ var onHideUsageTips: () -> Void = {}
var onUsageTipIndexChange: (Int) -> Void = { _ in }
var onUsageTipsHoverChange: (Bool) -> Void = { _ in }
@@ -184,6 +187,7 @@
onGroupActivate: @escaping (String) -> Void,
onScrollActivity: @escaping () -> Void,
onDragModeChange: @escaping (Bool) -> Void,
+ onHideUsageTips: @escaping () -> Void,
onUsageTipIndexChange: @escaping (Int) -> Void,
onUsageTipsHoverChange: @escaping (Bool) -> Void
) {
@@ -212,6 +216,7 @@
self.onGroupActivate = onGroupActivate
self.onScrollActivity = onScrollActivity
self.onDragModeChange = onDragModeChange
+ self.onHideUsageTips = onHideUsageTips
self.onUsageTipIndexChange = onUsageTipIndexChange
self.onUsageTipsHoverChange = onUsageTipsHoverChange
if !self.usageTipsVisible {
@@ -838,10 +843,12 @@
weak var coordinator: AppGridCollectionView.Coordinator?
private let backgroundView = NSVisualEffectView()
+ private let titlePanelView = NSView()
private let iconView = AppGridDecorativeImageView()
private let titleLabel = NSTextField(labelWithString: "")
private let detailScrollView = NSScrollView()
private let detailLabel = NSTextField(labelWithString: "")
+ private let closeButton = AppGridUsageTipIconButton(systemImage: "xmark")
private let previousButton = AppGridUsageTipIconButton(systemImage: "chevron.left")
private let nextButton = AppGridUsageTipIconButton(systemImage: "chevron.right")
private let dotsView = AppGridUsageTipDotsView()
@@ -850,9 +857,10 @@
private var visualWidth: CGFloat = AppGridUsageTipsMetrics.minWidth
private var visualHeight: CGFloat = AppGridUsageTipsMetrics.barHeight
private var visualBottomMargin: CGFloat = AppGridUsageTipsMetrics.bottomMargin
+ private var currentDetailText = ""
- private let titleFont = NSFont.systemFont(ofSize: 24, weight: .semibold)
- private let detailFont = NSFont.systemFont(ofSize: 24, weight: .regular)
+ private let titleFont = NSFont.systemFont(ofSize: 25, weight: .bold)
+ private let detailFont = NSFont.systemFont(ofSize: 24, weight: .medium)
override var isFlipped: Bool { true }
override var acceptsFirstResponder: Bool { true }
@@ -954,41 +962,65 @@
let visualFrame = currentVisualFrame()
backgroundView.frame = visualFrame
- let paddingLeft: CGFloat = 22
- let paddingRight: CGFloat = 16
- let iconSize: CGFloat = 24
- let iconTextGap: CGFloat = 10
- let controlZoneWidth: CGFloat = 176
- let buttonGap: CGFloat = 6
- let buttonSize: CGFloat = 48
- let buttonDotsGap: CGFloat = 6
+ let outerPaddingX: CGFloat = 20
+ let outerPaddingY: CGFloat = 19
+ let titlePanelWidth = min(
+ max(350, visualFrame.width * 0.24),
+ min(460, visualFrame.width * 0.36)
+ )
+ let titlePanelHeight = max(1, min(116, visualFrame.height - outerPaddingY * 2))
+ let titlePanelFrame = NSRect(
+ x: visualFrame.minX + outerPaddingX,
+ y: visualFrame.midY - titlePanelHeight / 2,
+ width: titlePanelWidth,
+ height: titlePanelHeight
+ )
+ titlePanelView.frame = titlePanelFrame
+
+ let iconSize: CGFloat = 26
+ let controlZoneWidth: CGFloat = min(148, max(128, visualFrame.width * 0.08))
+ let buttonGap: CGFloat = 12
+ let buttonSize: CGFloat = 52
+ let buttonDotsGap: CGFloat = 10
let dotsHeight: CGFloat = 10
let dotsWidth = dotsView.preferredWidth
- let titleX = visualFrame.minX + paddingLeft + iconSize + iconTextGap
+ let titleIconGap: CGFloat = 20
+ let titleSidePadding: CGFloat = 28
+ let titleX = titlePanelFrame.minX + titleSidePadding + iconSize + titleIconGap
+ let titleWidth = max(1, titlePanelFrame.maxX - titleX - titleSidePadding)
+ let titleHeight: CGFloat = 68
+ let titleY = titlePanelFrame.midY - titleHeight / 2
+ let detailGap = visualFrame.width < 980 ? CGFloat(30) : CGFloat(52)
+ let controlGap = visualFrame.width < 980 ? CGFloat(24) : CGFloat(36)
+ let detailX = titlePanelFrame.maxX + detailGap
+ let controlsMaxX = visualFrame.maxX - 54
+ let textRight = controlsMaxX - controlZoneWidth - controlGap
+ let detailHeight: CGFloat = 88
+ let detailY = visualFrame.minY + max(0, (visualFrame.height - detailHeight) / 2)
+
let buttonsWidth = buttonSize * 2 + buttonGap
- let buttonsGroupMinX = visualFrame.maxX - paddingRight - buttonsWidth
- let textRight = visualFrame.maxX - controlZoneWidth
- let availableTextWidth = max(1, textRight - titleX)
- let titleWidth = availableTextWidth
- let titleHeight: CGFloat = 32
- let detailHeight: CGFloat = 68
- let lineGap: CGFloat = 8
- let textBlockHeight = titleHeight + lineGap + detailHeight
- let textBlockY = visualFrame.minY + max(0, (visualFrame.height - textBlockHeight) / 2)
+ let buttonsGroupMinX = controlsMaxX - buttonsWidth
iconView.frame = NSRect(
- x: visualFrame.minX + paddingLeft,
- y: visualFrame.midY - iconSize / 2,
+ x: titlePanelFrame.minX + titleSidePadding,
+ y: titlePanelFrame.midY - iconSize / 2,
width: iconSize,
height: iconSize
)
titleLabel.frame = NSRect(
x: titleX,
- y: textBlockY,
+ y: titleY,
width: titleWidth,
height: titleHeight
+ )
+
+ closeButton.frame = NSRect(
+ x: visualFrame.maxX - 40,
+ y: visualFrame.minY + 10,
+ width: 30,
+ height: 30
)
let controlGroupHeight = buttonSize + buttonDotsGap + dotsHeight
@@ -1012,12 +1044,10 @@
height: dotsHeight
)
- let detailX = titleX
- let detailRight = textRight
detailScrollView.frame = NSRect(
x: detailX,
- y: titleLabel.frame.maxY + lineGap,
- width: max(1, detailRight - detailX),
+ y: detailY,
+ width: max(1, textRight - detailX),
height: detailHeight
)
layoutDetailLabel()
@@ -1037,7 +1067,9 @@
let safeIndex = min(max(0, coordinator.selectedUsageTipIndex), coordinator.usageTips.count - 1)
let tip = coordinator.usageTips[safeIndex]
titleLabel.stringValue = tr(tip.titleKey)
- detailLabel.stringValue = formattedTipDetail(tr(tip.detailKey))
+ updateTipIcon(id: tip.id)
+ currentDetailText = formattedTipDetail(tr(tip.detailKey))
+ closeButton.buttonAccessibilityLabel = tr("usageTips.close")
previousButton.buttonAccessibilityLabel = tr("usageTips.previous")
nextButton.buttonAccessibilityLabel = tr("usageTips.next")
dotsView.configure(count: coordinator.usageTips.count, selectedIndex: safeIndex)
@@ -1059,13 +1091,7 @@
}
func preferredWidth(maxAvailableWidth: CGFloat) -> CGFloat {
- let padding: CGFloat = 22 + 16
- let fixedWidth: CGFloat = 24 + 10 + 24 + 48 + 6 + 48
- let titleWidth = ceil(titleLabel.attributedStringValue.size().width)
- let detailWidth = measuredDetailLineWidth()
- let contentWidth = padding + fixedWidth + max(titleWidth, detailWidth)
- let preferredWidth = max(AppGridUsageTipsMetrics.minWidth, contentWidth)
- return min(maxAvailableWidth, preferredWidth)
+ max(1, maxAvailableWidth)
}
private func setup() {
@@ -1084,7 +1110,13 @@
backgroundView.layer?.borderWidth = 1
addSubview(backgroundView)
- let iconConfig = NSImage.SymbolConfiguration(pointSize: 24, weight: .semibold)
+ titlePanelView.wantsLayer = true
+ titlePanelView.layer?.cornerRadius = 18
+ titlePanelView.layer?.masksToBounds = true
+ titlePanelView.layer?.borderWidth = 1
+ addSubview(titlePanelView)
+
+ let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold)
iconView.image = NSImage(
systemSymbolName: "lightbulb.fill",
accessibilityDescription: nil
@@ -1092,7 +1124,7 @@
iconView.imageScaling = .scaleProportionallyDown
addSubview(iconView)
- configureLabel(titleLabel, font: titleFont, lineBreakMode: .byClipping)
+ configureLabel(titleLabel, font: titleFont, lineBreakMode: .byWordWrapping)
configureDetailLabel()
detailScrollView.drawsBackground = false
@@ -1102,12 +1134,14 @@
detailScrollView.borderType = .noBorder
detailScrollView.documentView = detailLabel
+ closeButton.action = { [weak self] in self?.hideUsageTips() }
previousButton.action = { [weak self] in self?.selectUsageTip(offset: -1) }
nextButton.action = { [weak self] in self?.selectUsageTip(offset: 1) }
addSubview(titleLabel)
addSubview(detailScrollView)
addSubview(dotsView)
+ addSubview(closeButton)
addSubview(previousButton)
addSubview(nextButton)
@@ -1122,13 +1156,13 @@
label.drawsBackground = false
label.isBordered = false
label.lineBreakMode = lineBreakMode
- label.maximumNumberOfLines = 1
- label.alignment = .center
+ label.maximumNumberOfLines = 2
+ label.alignment = .left
label.font = font
}
private func configureDetailLabel() {
- detailLabel.cell = NSTextFieldCell(textCell: "")
+ detailLabel.cell = AppGridCenteredMultilineTextFieldCell(textCell: "")
detailLabel.isEditable = false
detailLabel.isSelectable = false
detailLabel.drawsBackground = false
@@ -1160,9 +1194,22 @@
applyCoordinatorState()
}
+ private func hideUsageTips() {
+ Diagnostics.log("usageTips.hud.close")
+ clearHoverState()
+ isHidden = true
+ coordinator?.onHideUsageTips()
+ }
+
private func routeButtonClickIfNeeded(_ event: NSEvent) -> Bool {
let point = usageTipsPoint(for: event)
- let hitOutset: CGFloat = 24
+ let closeHitOutset: CGFloat = 8
+ if closeButton.frame.insetBy(dx: -closeHitOutset, dy: -closeHitOutset).contains(point) {
+ closeButton.performPressFeedback()
+ hideUsageTips()
+ return true
+ }
+ let hitOutset: CGFloat = 6
if nextButton.frame.insetBy(dx: -hitOutset, dy: -hitOutset).contains(point) {
Diagnostics.log("usageTips.hud.routedNext")
selectUsageTip(offset: 1)
@@ -1211,14 +1258,6 @@
detailLabel.frame = NSRect(x: 0, y: 0, width: width, height: detailScrollView.bounds.height)
}
- private func measuredDetailLineWidth() -> CGFloat {
- let attributes: [NSAttributedString.Key: Any] = [.font: detailFont]
- let lineWidths = detailLabel.stringValue
- .components(separatedBy: .newlines)
- .map { NSAttributedString(string: $0, attributes: attributes).size().width }
- return ceil(lineWidths.max() ?? detailLabel.attributedStringValue.size().width)
- }
-
private func currentVisualFrame() -> NSRect {
let width = min(bounds.width, visualWidth)
let height = min(bounds.height, visualHeight)
@@ -1231,21 +1270,197 @@
}
private func formattedTipDetail(_ text: String) -> String {
- text
+ let normalized = text
.replacingOccurrences(of: "\\s*(?:-->|->|>|→|>|,)\\s*", with: "\n", options: .regularExpression)
.replacingOccurrences(of: "[ \\t]{2,}", with: " ", options: .regularExpression)
.replacingOccurrences(of: "\\n{2,}", with: "\n", options: .regularExpression)
.trimmingCharacters(in: .whitespacesAndNewlines)
+ return orderedTipDetail(normalized)
+ }
+
+ private func orderedTipDetail(_ text: String) -> String {
+ let lines = text
+ .components(separatedBy: .newlines)
+ .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
+ .filter { !$0.isEmpty }
+ guard lines.count > 1 else { return text }
+ return lines
+ .enumerated()
+ .map { "\($0.offset + 1). \($0.element)" }
+ .joined(separator: "\n")
+ }
+
+ private func updateTipIcon(id: Int) {
+ let symbolName: String
+ switch id {
+ case 1:
+ symbolName = "tag.fill"
+ case 2:
+ symbolName = "checkmark.seal.fill"
+ case 3:
+ symbolName = "arrow.up.and.down.and.arrow.left.and.right"
+ case 4:
+ symbolName = "doc.on.doc.fill"
+ case 5:
+ symbolName = "minus.circle.fill"
+ case 6:
+ symbolName = "arrow.up.arrow.down.circle.fill"
+ case 7:
+ symbolName = "square.grid.2x2.fill"
+ case 8:
+ symbolName = "note.text"
+ default:
+ symbolName = "lightbulb.fill"
+ }
+ let iconConfig = NSImage.SymbolConfiguration(pointSize: 26, weight: .bold)
+ iconView.image = NSImage(
+ systemSymbolName: symbolName,
+ accessibilityDescription: nil
+ )?.withSymbolConfiguration(iconConfig)
+ ?? NSImage(
+ systemSymbolName: "lightbulb.fill",
+ accessibilityDescription: nil
+ )?.withSymbolConfiguration(iconConfig)
}
private func updateColors() {
- backgroundView.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.35).cgColor
- iconView.contentTintColor = .systemYellow
- titleLabel.textColor = .secondaryLabelColor
- detailLabel.textColor = .labelColor
- previousButton.tintColor = .labelColor
- nextButton.tintColor = .labelColor
- dotsView.needsDisplay = true
+ let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true
+
+ backgroundView.material = usesDarkGlass ? .underWindowBackground : .popover
+ backgroundView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil
+ backgroundView.layer?.backgroundColor = (
+ usesDarkGlass
+ ? NSColor.black.withAlphaComponent(0.46)
+ : NSColor.white.withAlphaComponent(0.54)
+ ).cgColor
+ backgroundView.layer?.borderColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.22)
+ : NSColor.black.withAlphaComponent(0.18)
+ ).cgColor
+
+ titlePanelView.layer?.backgroundColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.08)
+ : NSColor.white.withAlphaComponent(0.42)
+ ).cgColor
+ titlePanelView.layer?.borderColor = (
+ usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.12)
+ : NSColor.black.withAlphaComponent(0.08)
+ ).cgColor
+
+ let theme = coordinator?.appGridTheme ?? .fallback
+ let accentColor = usageTipAccentColor(for: theme)
+ iconView.contentTintColor = accentColor
+ titleLabel.textColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.92)
+ : accentColor.withAlphaComponent(0.94)
+ detailLabel.textColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.95)
+ : NSColor(calibratedRed: 0.08, green: 0.10, blue: 0.13, alpha: 0.94)
+ applyDetailTextStyle()
+
+ let buttonTint = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.90)
+ : NSColor(calibratedWhite: 0.08, alpha: 0.92)
+ let buttonHoverTint = usesDarkGlass
+ ? NSColor.white
+ : NSColor(calibratedWhite: 0.04, alpha: 1.00)
+ let buttonPressedTint = usesDarkGlass
+ ? NSColor.white
+ : NSColor.black
+ let buttonSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.10)
+ : NSColor.white.withAlphaComponent(0.38)
+ let buttonHoverSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.20)
+ : NSColor.white.withAlphaComponent(0.70)
+ let buttonPressedSurface = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.28)
+ : NSColor.white.withAlphaComponent(0.86)
+ let buttonBorder = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.14)
+ : NSColor.black.withAlphaComponent(0.10)
+ [previousButton, nextButton].forEach { button in
+ button.tintColor = buttonTint
+ button.hoveredTintColor = buttonHoverTint
+ button.pressedTintColor = buttonPressedTint
+ button.surfaceColor = buttonSurface
+ button.hoveredSurfaceColor = buttonHoverSurface
+ button.pressedSurfaceColor = buttonPressedSurface
+ button.borderColor = buttonBorder
+ }
+
+ closeButton.tintColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.42)
+ : NSColor.black.withAlphaComponent(0.34)
+ closeButton.hoveredTintColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.86)
+ : NSColor.black.withAlphaComponent(0.78)
+ closeButton.pressedTintColor = usesDarkGlass ? .white : .black
+ closeButton.surfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.04)
+ : NSColor.white.withAlphaComponent(0.08)
+ closeButton.hoveredSurfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.14)
+ : NSColor.white.withAlphaComponent(0.62)
+ closeButton.pressedSurfaceColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.22)
+ : NSColor.white.withAlphaComponent(0.82)
+ closeButton.borderColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.08)
+ : NSColor.black.withAlphaComponent(0.08)
+
+ dotsView.selectedColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.88)
+ : NSColor.labelColor.withAlphaComponent(0.74)
+ dotsView.idleColor = usesDarkGlass
+ ? NSColor.white.withAlphaComponent(0.28)
+ : NSColor.labelColor.withAlphaComponent(0.24)
+
+ layer?.shadowOpacity = usesDarkGlass ? 0.28 : 0.14
+ layer?.shadowRadius = usesDarkGlass ? 18 : 12
+ needsDisplay = true
+ }
+
+ private func usageTipAccentColor(for theme: AppGridTheme) -> NSColor {
+ switch theme {
+ case .defaultLight:
+ return NSColor(calibratedRed: 0.34, green: 0.40, blue: 0.48, alpha: 1.00)
+ case .deepBlue:
+ return NSColor(calibratedRed: 0.42, green: 0.80, blue: 1.00, alpha: 1.00)
+ case .black:
+ return NSColor(calibratedRed: 0.36, green: 0.74, blue: 1.00, alpha: 1.00)
+ case .pink:
+ return NSColor(calibratedRed: 0.70, green: 0.18, blue: 0.45, alpha: 1.00)
+ case .purple:
+ return NSColor(calibratedRed: 0.40, green: 0.22, blue: 0.70, alpha: 1.00)
+ case .green:
+ return NSColor(calibratedRed: 0.08, green: 0.42, blue: 0.22, alpha: 1.00)
+ case .blue:
+ return NSColor(calibratedRed: 0.06, green: 0.34, blue: 0.74, alpha: 1.00)
+ case .colorful:
+ return NSColor(calibratedRed: 0.06, green: 0.44, blue: 0.78, alpha: 1.00)
+ }
+ }
+
+ private func applyDetailTextStyle() {
+ let paragraphStyle = NSMutableParagraphStyle()
+ paragraphStyle.alignment = .left
+ paragraphStyle.lineBreakMode = .byWordWrapping
+ paragraphStyle.minimumLineHeight = 31
+ paragraphStyle.maximumLineHeight = 34
+ paragraphStyle.lineSpacing = 6
+
+ detailLabel.attributedStringValue = NSAttributedString(
+ string: currentDetailText,
+ attributes: [
+ .font: detailFont,
+ .foregroundColor: detailLabel.textColor ?? NSColor.labelColor,
+ .paragraphStyle: paragraphStyle
+ ]
+ )
}
}
@@ -1255,6 +1470,20 @@
let textHeight = cellSize(forBounds: rect).height
drawingRect.origin.y = rect.origin.y + max(0, (rect.height - textHeight) / 2)
drawingRect.size.height = min(rect.height, textHeight + 2)
+ return drawingRect
+ }
+}
+
+private final class AppGridCenteredMultilineTextFieldCell: NSTextFieldCell {
+ override func drawingRect(forBounds rect: NSRect) -> NSRect {
+ var drawingRect = super.drawingRect(forBounds: rect)
+ let textHeight = attributedStringValue.boundingRect(
+ with: NSSize(width: max(1, rect.width), height: .greatestFiniteMagnitude),
+ options: [.usesLineFragmentOrigin, .usesFontLeading]
+ ).height
+ let centeredHeight = min(rect.height, ceil(textHeight) + 4)
+ drawingRect.origin.y = rect.origin.y + max(0, (rect.height - centeredHeight) / 2)
+ drawingRect.size.height = centeredHeight
return drawingRect
}
}
@@ -1278,8 +1507,26 @@
}
var tintColor: NSColor = .labelColor {
didSet {
- imageView.contentTintColor = tintColor
+ updateIconTint()
}
+ }
+ var hoveredTintColor: NSColor? {
+ didSet { updateIconTint() }
+ }
+ var pressedTintColor: NSColor? {
+ didSet { updateIconTint() }
+ }
+ var surfaceColor: NSColor = .clear {
+ didSet { needsDisplay = true }
+ }
+ var hoveredSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.08) {
+ didSet { needsDisplay = true }
+ }
+ var pressedSurfaceColor: NSColor = NSColor.labelColor.withAlphaComponent(0.14) {
+ didSet { needsDisplay = true }
+ }
+ var borderColor: NSColor = .clear {
+ didSet { needsDisplay = true }
}
private let imageView = AppGridDecorativeImageView()
@@ -1319,9 +1566,20 @@
return true
}
+ func performPressFeedback() {
+ isPressed = true
+ needsDisplay = true
+ updateIconTint()
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in
+ self?.isPressed = false
+ self?.needsDisplay = true
+ self?.updateIconTint()
+ }
+ }
+
override func layout() {
super.layout()
- let size: CGFloat = 26
+ let size = min(CGFloat(26), max(14, min(bounds.width, bounds.height) * 0.62))
imageView.frame = NSRect(
x: (bounds.width - size) / 2,
y: (bounds.height - size) / 2,
@@ -1352,31 +1610,32 @@
override func mouseEntered(with event: NSEvent) {
isHovered = true
needsDisplay = true
+ updateIconTint()
}
override func mouseExited(with event: NSEvent) {
isHovered = false
isPressed = false
needsDisplay = true
+ updateIconTint()
}
override func mouseDown(with event: NSEvent) {
Diagnostics.log("usageTips.button.mouseDown", [
"label": buttonAccessibilityLabel
])
- isPressed = true
- needsDisplay = true
+ performPressFeedback()
action?()
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.10) { [weak self] in
- self?.isPressed = false
- self?.needsDisplay = true
- }
}
override func draw(_ dirtyRect: NSRect) {
- guard isHovered || isPressed else { return }
- tintColor.withAlphaComponent(isPressed ? 0.14 : 0.08).setFill()
- NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 6, yRadius: 6).fill()
+ let path = NSBezierPath(roundedRect: bounds.insetBy(dx: 1, dy: 1), xRadius: 8, yRadius: 8)
+ let fill = isPressed ? pressedSurfaceColor : isHovered ? hoveredSurfaceColor : surfaceColor
+ fill.setFill()
+ path.fill()
+ borderColor.setStroke()
+ path.lineWidth = 1
+ path.stroke()
}
private func setup(systemImage: String) {
@@ -1391,11 +1650,27 @@
addSubview(imageView)
setAccessibilityRole(.button)
}
+
+ private func updateIconTint() {
+ if isPressed, let pressedTintColor {
+ imageView.contentTintColor = pressedTintColor
+ } else if isHovered, let hoveredTintColor {
+ imageView.contentTintColor = hoveredTintColor
+ } else {
+ imageView.contentTintColor = tintColor
+ }
+ }
}
private final class AppGridUsageTipDotsView: NSView {
private var count = 0
private var selectedIndex = 0
+ var selectedColor: NSColor = .labelColor {
+ didSet { needsDisplay = true }
+ }
+ var idleColor: NSColor = NSColor.labelColor.withAlphaComponent(0.28) {
+ didSet { needsDisplay = true }
+ }
override var isFlipped: Bool { true }
@@ -1420,7 +1695,7 @@
for index in 0..<count {
let selected = index == selectedIndex
let dotSize: CGFloat = selected ? 6 : 5
- let color = NSColor.labelColor.withAlphaComponent(selected ? 0.85 : 0.28)
+ let color = selected ? selectedColor : idleColor
color.setFill()
NSBezierPath(
ovalIn: NSRect(
diff --git a/src/Apptag/ContentView.swift b/src/Apptag/ContentView.swift
index 87ffec3..9f0743c 100644
--- a/src/Apptag/ContentView.swift
+++ b/src/Apptag/ContentView.swift
@@ -1242,6 +1242,7 @@
},
onScrollActivity: handleAppGridScrollActivity,
onDragModeChange: { setAppDragMode($0) },
+ onHideUsageTips: { hideUsageTips = true },
onUsageTipsHoverChange: handleUsageTipsHoverChange
)
}
diff --git a/src/Apptag/Info.plist b/src/Apptag/Info.plist
index 3e3fce8..9b25745 100644
--- a/src/Apptag/Info.plist
+++ b/src/Apptag/Info.plist
@@ -19,9 +19,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>8.0.1</string>
+ <string>8.0.2</string>
<key>CFBundleVersion</key>
- <string>20260624.2021</string>
+ <string>20260624.2341</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSMinimumSystemVersion</key>
diff --git a/src/Apptag/Localization/ar-Najdi.json b/src/Apptag/Localization/ar-Najdi.json
index 9280fba..aecd821 100644
--- a/src/Apptag/Localization/ar-Najdi.json
+++ b/src/Apptag/Localization/ar-Najdi.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "إذا شغّلته، تختفي التلميحات العايمة أسفل شبكة التطبيقات.",
"usageTips.previous": "التلميحة السابقة",
"usageTips.next": "التلميحة التالية",
- "usageTips.tip1.title": "تلميحة 1 - إضافة وسوم",
- "usageTips.tip1.detail": "افتح الإعدادات, عدّل الوسوم.",
+ "usageTips.close": "إغلاق التلميحات",
+ "usageTips.tip1.title": "تلميحة 1 - تحرير الوسوم",
+ "usageTips.tip1.detail": "افتح الإعدادات وانتقل إلى الوسوم للتحرير, انقر نقرًا مزدوجًا على وسم في قائمة الوسوم لفتح تحرير الوسوم بسرعة.",
"usageTips.tip2.title": "تلميحة 2 - وسم التطبيقات",
"usageTips.tip2.detail": "اضغط مطوّل على التطبيق, اسحبه على وسم.",
"usageTips.tip3.title": "تلميحة 3 - نقل التطبيقات",
diff --git a/src/Apptag/Localization/ar.json b/src/Apptag/Localization/ar.json
index 631b3e1..ed8b24e 100644
--- a/src/Apptag/Localization/ar.json
+++ b/src/Apptag/Localization/ar.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "عند التفعيل، يتم إخفاء النصائح العائمة أسفل شبكة التطبيقات.",
"usageTips.previous": "النصيحة السابقة",
"usageTips.next": "النصيحة التالية",
- "usageTips.tip1.title": "النصيحة 1 - إضافة وسوم",
- "usageTips.tip1.detail": "افتح الإعدادات, حرر الوسوم.",
+ "usageTips.close": "إغلاق التلميحات",
+ "usageTips.tip1.title": "النصيحة 1 - تحرير الوسوم",
+ "usageTips.tip1.detail": "افتح الإعدادات وانتقل إلى الوسوم للتحرير, انقر نقرًا مزدوجًا على وسم في قائمة الوسوم لفتح تحرير الوسوم بسرعة.",
"usageTips.tip2.title": "النصيحة 2 - وسم التطبيقات",
"usageTips.tip2.detail": "اضغط مطولاً على تطبيق, اسحبه إلى وسم.",
"usageTips.tip3.title": "النصيحة 3 - نقل التطبيقات",
diff --git a/src/Apptag/Localization/cs.json b/src/Apptag/Localization/cs.json
index 2c1c042..373c3f7 100644
--- a/src/Apptag/Localization/cs.json
+++ b/src/Apptag/Localization/cs.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Když je zapnuto, plovoucí tipy dole v mřížce aplikací se skryjí.",
"usageTips.previous": "Předchozí tip",
"usageTips.next": "Další tip",
- "usageTips.tip1.title": "Tip 1 - Přidat tagy",
- "usageTips.tip1.detail": "Otevřete Nastavení, upravte tagy.",
+ "usageTips.close": "Zavřít tipy",
+ "usageTips.tip1.title": "Tip 1 - Upravit tagy",
+ "usageTips.tip1.detail": "Otevřete Nastavení a přepněte na Tagy pro úpravy, Dvojklikem na tag v seznamu rychle otevřete úpravy tagů.",
"usageTips.tip2.title": "Tip 2 - Označit aplikace",
"usageTips.tip2.detail": "Podržte aplikaci, přetáhněte ji na tag.",
"usageTips.tip3.title": "Tip 3 - Přesunout aplikace",
diff --git a/src/Apptag/Localization/da.json b/src/Apptag/Localization/da.json
index 8df55ad..5aac946 100644
--- a/src/Apptag/Localization/da.json
+++ b/src/Apptag/Localization/da.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Når det er slået til, skjules de flydende tips nederst i appgitteret.",
"usageTips.previous": "Forrige tip",
"usageTips.next": "Næste tip",
- "usageTips.tip1.title": "Tip 1 - Tilføj tags",
- "usageTips.tip1.detail": "Åbn Indstillinger, rediger tags.",
+ "usageTips.close": "Luk tips",
+ "usageTips.tip1.title": "Tip 1 - Rediger tags",
+ "usageTips.tip1.detail": "Åbn Indstillinger og skift til Tags for at redigere, Dobbeltklik på et tag i taglisten for hurtigt at åbne tagredigering.",
"usageTips.tip2.title": "Tip 2 - Tag apps",
"usageTips.tip2.detail": "Hold en app nede, slip den på et tag.",
"usageTips.tip3.title": "Tip 3 - Flyt apps",
diff --git a/src/Apptag/Localization/de.json b/src/Apptag/Localization/de.json
index 1bf578f..ff27951 100644
--- a/src/Apptag/Localization/de.json
+++ b/src/Apptag/Localization/de.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Wenn aktiviert, werden die schwebenden Tipps unten im App-Raster ausgeblendet.",
"usageTips.previous": "Vorheriger Tipp",
"usageTips.next": "Nächster Tipp",
- "usageTips.tip1.title": "Tipp 1 - Tags hinzufügen",
- "usageTips.tip1.detail": "Einstellungen öffnen, Tags bearbeiten.",
+ "usageTips.close": "Tipps schließen",
+ "usageTips.tip1.title": "Tipp 1 - Tags bearbeiten",
+ "usageTips.tip1.detail": "Öffnen Sie Einstellungen und wechseln Sie zu Tags zum Bearbeiten, Doppelklicken Sie in der Tagliste auf einen Tag zum schnellen Bearbeiten.",
"usageTips.tip2.title": "Tipp 2 - Apps taggen",
"usageTips.tip2.detail": "App gedrückt halten, auf einen Tag ziehen.",
"usageTips.tip3.title": "Tipp 3 - Apps verschieben",
diff --git a/src/Apptag/Localization/en.json b/src/Apptag/Localization/en.json
index 78b7151..b9e414d 100644
--- a/src/Apptag/Localization/en.json
+++ b/src/Apptag/Localization/en.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "When enabled, the floating tips at the bottom of the app grid are hidden.",
"usageTips.previous": "Previous tip",
"usageTips.next": "Next tip",
- "usageTips.tip1.title": "Tip 1 - Add tags",
- "usageTips.tip1.detail": "Open Settings, edit tags.",
+ "usageTips.close": "Close tips",
+ "usageTips.tip1.title": "Tip 1 - Edit tags",
+ "usageTips.tip1.detail": "Open Settings and switch to Tags to edit, Double-click a tag in the tag list to open tag editing quickly.",
"usageTips.tip2.title": "Tip 2 - Tag apps",
"usageTips.tip2.detail": "Long-press an app, then drag it onto a tag.",
"usageTips.tip3.title": "Tip 3 - Move apps",
diff --git a/src/Apptag/Localization/es.json b/src/Apptag/Localization/es.json
index 1a39921..504e016 100644
--- a/src/Apptag/Localization/es.json
+++ b/src/Apptag/Localization/es.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Si está activado, se ocultan los consejos flotantes de la parte inferior de la cuadrícula de apps.",
"usageTips.previous": "Consejo anterior",
"usageTips.next": "Consejo siguiente",
- "usageTips.tip1.title": "Consejo 1 - Añadir etiquetas",
- "usageTips.tip1.detail": "Abra Ajustes, edite etiquetas.",
+ "usageTips.close": "Cerrar consejos",
+ "usageTips.tip1.title": "Consejo 1 - Editar etiquetas",
+ "usageTips.tip1.detail": "Abra Ajustes y cambie a Etiquetas para editar, Haga doble clic en una etiqueta de la lista para abrir rápido la edición.",
"usageTips.tip2.title": "Consejo 2 - Etiquetar apps",
"usageTips.tip2.detail": "Mantén pulsada una app, arrástrala a una etiqueta.",
"usageTips.tip3.title": "Consejo 3 - Mover apps",
diff --git a/src/Apptag/Localization/fr.json b/src/Apptag/Localization/fr.json
index 8bb733f..ce30d53 100644
--- a/src/Apptag/Localization/fr.json
+++ b/src/Apptag/Localization/fr.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Si cette option est activée, les astuces flottantes en bas de la grille des apps sont masquées.",
"usageTips.previous": "Astuce précédente",
"usageTips.next": "Astuce suivante",
- "usageTips.tip1.title": "Astuce 1 - Ajouter des tags",
- "usageTips.tip1.detail": "Ouvrez Réglages, modifiez les tags.",
+ "usageTips.close": "Fermer les astuces",
+ "usageTips.tip1.title": "Astuce 1 - Modifier les tags",
+ "usageTips.tip1.detail": "Ouvrez Réglages et passez à Tags pour modifier, Double-cliquez sur un tag dans la liste pour ouvrir vite l’édition.",
"usageTips.tip2.title": "Astuce 2 - Taguer des apps",
"usageTips.tip2.detail": "Appuyez longuement sur une app, déposez-la sur un tag.",
"usageTips.tip3.title": "Astuce 3 - Déplacer des apps",
diff --git a/src/Apptag/Localization/id.json b/src/Apptag/Localization/id.json
index 42fb22d..83a2029 100644
--- a/src/Apptag/Localization/id.json
+++ b/src/Apptag/Localization/id.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Jika aktif, tips mengambang di bagian bawah grid app akan disembunyikan.",
"usageTips.previous": "Tips sebelumnya",
"usageTips.next": "Tips berikutnya",
- "usageTips.tip1.title": "Tips 1 - Tambah tag",
- "usageTips.tip1.detail": "Buka Pengaturan, edit tag.",
+ "usageTips.close": "Tutup tips",
+ "usageTips.tip1.title": "Tips 1 - Edit tag",
+ "usageTips.tip1.detail": "Buka Pengaturan lalu pindah ke Tag untuk mengedit, Klik dua kali tag di daftar tag untuk cepat membuka editor tag.",
"usageTips.tip2.title": "Tips 2 - Beri tag app",
"usageTips.tip2.detail": "Tekan lama app, jatuhkan ke tag.",
"usageTips.tip3.title": "Tips 3 - Pindahkan app",
diff --git a/src/Apptag/Localization/it.json b/src/Apptag/Localization/it.json
index 0acc575..4aec5ab 100644
--- a/src/Apptag/Localization/it.json
+++ b/src/Apptag/Localization/it.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Se attivo, i suggerimenti flottanti in basso nella griglia delle app vengono nascosti.",
"usageTips.previous": "Suggerimento precedente",
"usageTips.next": "Suggerimento successivo",
- "usageTips.tip1.title": "Suggerimento 1 - Aggiungere tag",
- "usageTips.tip1.detail": "Apri Impostazioni, modifica i tag.",
+ "usageTips.close": "Chiudi suggerimenti",
+ "usageTips.tip1.title": "Suggerimento 1 - Modificare i tag",
+ "usageTips.tip1.detail": "Apri Impostazioni e passa a Tag per modificare, Fai doppio clic su un tag nell’elenco per aprire rapidamente la modifica.",
"usageTips.tip2.title": "Suggerimento 2 - Taggare app",
"usageTips.tip2.detail": "Tieni premuta un’app, rilasciala su un tag.",
"usageTips.tip3.title": "Suggerimento 3 - Spostare app",
diff --git a/src/Apptag/Localization/ja.json b/src/Apptag/Localization/ja.json
index 24580dc..6fdd118 100644
--- a/src/Apptag/Localization/ja.json
+++ b/src/Apptag/Localization/ja.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "オンにすると、アプリグリッド下部のフローティングヒントを表示しません。",
"usageTips.previous": "前のヒント",
"usageTips.next": "次のヒント",
- "usageTips.tip1.title": "ヒント1 - タグを追加",
- "usageTips.tip1.detail": "設定を開く, タグを編集します。",
+ "usageTips.close": "ヒントを閉じる",
+ "usageTips.tip1.title": "ヒント1 - タグを編集",
+ "usageTips.tip1.detail": "設定を開いて「タグ」に切り替えて編集, タグ一覧でタグをダブルクリックすると編集画面をすぐ開けます。",
"usageTips.tip2.title": "ヒント2 - アプリにタグ付け",
"usageTips.tip2.detail": "アプリを長押し, タグ上にドロップします。",
"usageTips.tip3.title": "ヒント3 - アプリを移動",
diff --git a/src/Apptag/Localization/ko.json b/src/Apptag/Localization/ko.json
index bfcca64..61b14b8 100644
--- a/src/Apptag/Localization/ko.json
+++ b/src/Apptag/Localization/ko.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "켜면 앱 그리드 하단의 플로팅 사용 팁이 표시되지 않습니다.",
"usageTips.previous": "이전 팁",
"usageTips.next": "다음 팁",
- "usageTips.tip1.title": "팁 1 - 태그 추가",
- "usageTips.tip1.detail": "설정 열기, 태그 편집.",
+ "usageTips.close": "팁 닫기",
+ "usageTips.tip1.title": "팁 1 - 태그 편집",
+ "usageTips.tip1.detail": "설정을 열고 태그로 전환해 편집, 태그 목록에서 태그를 두 번 클릭하면 태그 편집으로 빠르게 이동합니다.",
"usageTips.tip2.title": "팁 2 - 앱에 태그 지정",
"usageTips.tip2.detail": "앱을 길게 누르기, 태그 위에 놓기.",
"usageTips.tip3.title": "팁 3 - 앱 이동",
diff --git a/src/Apptag/Localization/ms.json b/src/Apptag/Localization/ms.json
index aed2ea8..a678e40 100644
--- a/src/Apptag/Localization/ms.json
+++ b/src/Apptag/Localization/ms.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Apabila dihidupkan, petua terapung di bahagian bawah grid app akan disembunyikan.",
"usageTips.previous": "Petua sebelumnya",
"usageTips.next": "Petua seterusnya",
- "usageTips.tip1.title": "Petua 1 - Tambah tag",
- "usageTips.tip1.detail": "Buka Tetapan, edit tag.",
+ "usageTips.close": "Tutup petua",
+ "usageTips.tip1.title": "Petua 1 - Edit tag",
+ "usageTips.tip1.detail": "Buka Tetapan dan tukar ke Tag untuk mengedit, Dwiklik tag dalam senarai tag untuk cepat membuka editor tag.",
"usageTips.tip2.title": "Petua 2 - Tag app",
"usageTips.tip2.detail": "Tekan lama app, lepaskan pada tag.",
"usageTips.tip3.title": "Petua 3 - Alih app",
diff --git a/src/Apptag/Localization/nb.json b/src/Apptag/Localization/nb.json
index 037ffb7..ec86d9e 100644
--- a/src/Apptag/Localization/nb.json
+++ b/src/Apptag/Localization/nb.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Når dette er slått på, skjules de flytende tipsene nederst i app-rutenettet.",
"usageTips.previous": "Forrige tips",
"usageTips.next": "Neste tips",
- "usageTips.tip1.title": "Tips 1 - Legg til tagger",
- "usageTips.tip1.detail": "Åpne Innstillinger, rediger tagger.",
+ "usageTips.close": "Lukk tips",
+ "usageTips.tip1.title": "Tips 1 - Rediger tagger",
+ "usageTips.tip1.detail": "Åpne Innstillinger og bytt til Tagger for å redigere, Dobbeltklikk en tagg i tagglisten for raskt å åpne redigering.",
"usageTips.tip2.title": "Tips 2 - Tagg apper",
"usageTips.tip2.detail": "Hold en app inne, slipp den på en tagg.",
"usageTips.tip3.title": "Tips 3 - Flytt apper",
diff --git a/src/Apptag/Localization/nl.json b/src/Apptag/Localization/nl.json
index 5436b0d..7ed9eda 100644
--- a/src/Apptag/Localization/nl.json
+++ b/src/Apptag/Localization/nl.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Als dit is ingeschakeld, worden de zwevende tips onderaan het app-raster verborgen.",
"usageTips.previous": "Vorige tip",
"usageTips.next": "Volgende tip",
- "usageTips.tip1.title": "Tip 1 - Tags toevoegen",
- "usageTips.tip1.detail": "Open Instellingen, bewerk tags.",
+ "usageTips.close": "Tips sluiten",
+ "usageTips.tip1.title": "Tip 1 - Tags bewerken",
+ "usageTips.tip1.detail": "Open Instellingen en ga naar Tags om te bewerken, Dubbelklik op een tag in de lijst om tagbewerking snel te openen.",
"usageTips.tip2.title": "Tip 2 - Apps taggen",
"usageTips.tip2.detail": "Houd een app ingedrukt, sleep naar een tag.",
"usageTips.tip3.title": "Tip 3 - Apps verplaatsen",
diff --git a/src/Apptag/Localization/nn.json b/src/Apptag/Localization/nn.json
index e6671c0..acd04e1 100644
--- a/src/Apptag/Localization/nn.json
+++ b/src/Apptag/Localization/nn.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Når dette er slått på, vert dei flytande tipsa nedst i apprutenettet gøymde.",
"usageTips.previous": "Forrige tips",
"usageTips.next": "Neste tips",
- "usageTips.tip1.title": "Tips 1 - Legg til tagger",
- "usageTips.tip1.detail": "Opna Innstillingar, rediger taggar.",
+ "usageTips.close": "Lukk tips",
+ "usageTips.tip1.title": "Tips 1 - Rediger taggar",
+ "usageTips.tip1.detail": "Opna Innstillingar og byt til Taggar for å redigere, Dobbeltklikk ein tagg i tagglista for raskt å opne redigering.",
"usageTips.tip2.title": "Tips 2 - Tagg apper",
"usageTips.tip2.detail": "Hold ein app inne, slepp han på ein tagg.",
"usageTips.tip3.title": "Tips 3 - Flytt apper",
diff --git a/src/Apptag/Localization/no.json b/src/Apptag/Localization/no.json
index 037ffb7..ec86d9e 100644
--- a/src/Apptag/Localization/no.json
+++ b/src/Apptag/Localization/no.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Når dette er slått på, skjules de flytende tipsene nederst i app-rutenettet.",
"usageTips.previous": "Forrige tips",
"usageTips.next": "Neste tips",
- "usageTips.tip1.title": "Tips 1 - Legg til tagger",
- "usageTips.tip1.detail": "Åpne Innstillinger, rediger tagger.",
+ "usageTips.close": "Lukk tips",
+ "usageTips.tip1.title": "Tips 1 - Rediger tagger",
+ "usageTips.tip1.detail": "Åpne Innstillinger og bytt til Tagger for å redigere, Dobbeltklikk en tagg i tagglisten for raskt å åpne redigering.",
"usageTips.tip2.title": "Tips 2 - Tagg apper",
"usageTips.tip2.detail": "Hold en app inne, slipp den på en tagg.",
"usageTips.tip3.title": "Tips 3 - Flytt apper",
diff --git a/src/Apptag/Localization/pl.json b/src/Apptag/Localization/pl.json
index 8771abd..8404df6 100644
--- a/src/Apptag/Localization/pl.json
+++ b/src/Apptag/Localization/pl.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Po włączeniu pływające wskazówki u dołu siatki aplikacji będą ukryte.",
"usageTips.previous": "Poprzednia wskazówka",
"usageTips.next": "Następna wskazówka",
- "usageTips.tip1.title": "Wskazówka 1 - Dodaj tagi",
- "usageTips.tip1.detail": "Otwórz Ustawienia, edytuj tagi.",
+ "usageTips.close": "Zamknij wskazówki",
+ "usageTips.tip1.title": "Wskazówka 1 - Edytuj tagi",
+ "usageTips.tip1.detail": "Otwórz Ustawienia i przejdź do Tagów aby edytować, Kliknij dwukrotnie tag na liście aby szybko otworzyć edycję.",
"usageTips.tip2.title": "Wskazówka 2 - Otaguj aplikacje",
"usageTips.tip2.detail": "Przytrzymaj aplikację, przeciągnij ją na tag.",
"usageTips.tip3.title": "Wskazówka 3 - Przenieś aplikacje",
diff --git a/src/Apptag/Localization/pt-BR.json b/src/Apptag/Localization/pt-BR.json
index 0379664..0c357fd 100644
--- a/src/Apptag/Localization/pt-BR.json
+++ b/src/Apptag/Localization/pt-BR.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Quando ativado, as dicas flutuantes na parte inferior da grade de apps ficam ocultas.",
"usageTips.previous": "Dica anterior",
"usageTips.next": "Próxima dica",
- "usageTips.tip1.title": "Dica 1 - Adicionar tags",
- "usageTips.tip1.detail": "Abra Ajustes, edite tags.",
+ "usageTips.close": "Fechar dicas",
+ "usageTips.tip1.title": "Dica 1 - Editar tags",
+ "usageTips.tip1.detail": "Abra Ajustes e mude para Tags para editar, Clique duas vezes em uma tag na lista para abrir a edição rapidamente.",
"usageTips.tip2.title": "Dica 2 - Marcar apps",
"usageTips.tip2.detail": "Pressione e segure um app, solte sobre uma tag.",
"usageTips.tip3.title": "Dica 3 - Mover apps",
diff --git a/src/Apptag/Localization/ro.json b/src/Apptag/Localization/ro.json
index 252df0f..ef254b1 100644
--- a/src/Apptag/Localization/ro.json
+++ b/src/Apptag/Localization/ro.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Când este activat, sfaturile plutitoare din partea de jos a grilei de aplicații sunt ascunse.",
"usageTips.previous": "Sfatul anterior",
"usageTips.next": "Sfatul următor",
- "usageTips.tip1.title": "Sfat 1 - Adaugă etichete",
- "usageTips.tip1.detail": "Deschide Setări, editează etichetele.",
+ "usageTips.close": "Închide sfaturile",
+ "usageTips.tip1.title": "Sfat 1 - Editează etichete",
+ "usageTips.tip1.detail": "Deschide Setări și comută la Etichete pentru editare, Dublu clic pe o etichetă din listă pentru a deschide rapid editarea.",
"usageTips.tip2.title": "Sfat 2 - Etichetează aplicații",
"usageTips.tip2.detail": "Ține apăsată o aplicație, plaseaz-o pe o etichetă.",
"usageTips.tip3.title": "Sfat 3 - Mută aplicații",
diff --git a/src/Apptag/Localization/ru.json b/src/Apptag/Localization/ru.json
index 6acba70..60577a4 100644
--- a/src/Apptag/Localization/ru.json
+++ b/src/Apptag/Localization/ru.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Если включено, плавающие советы внизу сетки приложений будут скрыты.",
"usageTips.previous": "Предыдущий совет",
"usageTips.next": "Следующий совет",
- "usageTips.tip1.title": "Совет 1 — Добавить теги",
- "usageTips.tip1.detail": "Откройте Настройки, редактируйте теги.",
+ "usageTips.close": "Закрыть советы",
+ "usageTips.tip1.title": "Совет 1 — Редактировать теги",
+ "usageTips.tip1.detail": "Откройте Настройки и перейдите к Тегам для редактирования, Дважды щёлкните тег в списке чтобы быстро открыть редактирование.",
"usageTips.tip2.title": "Совет 2 — Назначить теги",
"usageTips.tip2.detail": "Удерживайте приложение, перетащите на тег.",
"usageTips.tip3.title": "Совет 3 — Переместить приложения",
diff --git a/src/Apptag/Localization/sr-Cyrl.json b/src/Apptag/Localization/sr-Cyrl.json
index ad7af39..dc48ed0 100644
--- a/src/Apptag/Localization/sr-Cyrl.json
+++ b/src/Apptag/Localization/sr-Cyrl.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Када је укључено, плутајући савети на дну мреже апликација су сакривени.",
"usageTips.previous": "Претходни савет",
"usageTips.next": "Следећи савет",
- "usageTips.tip1.title": "Савет 1 - Додај ознаке",
- "usageTips.tip1.detail": "Отворите Подешавања, уредите ознаке.",
+ "usageTips.close": "Затвори савете",
+ "usageTips.tip1.title": "Савет 1 - Уреди ознаке",
+ "usageTips.tip1.detail": "Отворите Подешавања и пређите на Ознаке за уређивање, Двапут кликните ознаку у листи за брзо отварање уређивања.",
"usageTips.tip2.title": "Савет 2 - Означи апликације",
"usageTips.tip2.detail": "Држите апликацију, превуците је на ознаку.",
"usageTips.tip3.title": "Савет 3 - Премести апликације",
diff --git a/src/Apptag/Localization/sv.json b/src/Apptag/Localization/sv.json
index 368bf08..f8a0ae1 100644
--- a/src/Apptag/Localization/sv.json
+++ b/src/Apptag/Localization/sv.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "När detta är aktiverat döljs de flytande tipsen längst ned i apprutnätet.",
"usageTips.previous": "Föregående tips",
"usageTips.next": "Nästa tips",
- "usageTips.tip1.title": "Tips 1 - Lägg till taggar",
- "usageTips.tip1.detail": "Öppna Inställningar, redigera taggar.",
+ "usageTips.close": "Stäng tips",
+ "usageTips.tip1.title": "Tips 1 - Redigera taggar",
+ "usageTips.tip1.detail": "Öppna Inställningar och växla till Taggar för att redigera, Dubbelklicka på en tagg i tagglistan för att snabbt öppna redigering.",
"usageTips.tip2.title": "Tips 2 - Tagga appar",
"usageTips.tip2.detail": "Håll ned en app, släpp den på en tagg.",
"usageTips.tip3.title": "Tips 3 - Flytta appar",
diff --git a/src/Apptag/Localization/th.json b/src/Apptag/Localization/th.json
index b0b8205..d97847e 100644
--- a/src/Apptag/Localization/th.json
+++ b/src/Apptag/Localization/th.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "เมื่อเปิดใช้ เคล็ดลับแบบลอยที่ด้านล่างของกริดแอปจะถูกซ่อน",
"usageTips.previous": "เคล็ดลับก่อนหน้า",
"usageTips.next": "เคล็ดลับถัดไป",
- "usageTips.tip1.title": "เคล็ดลับ 1 - เพิ่มแท็ก",
- "usageTips.tip1.detail": "เปิดการตั้งค่า, แก้ไขแท็ก",
+ "usageTips.close": "ปิดเคล็ดลับ",
+ "usageTips.tip1.title": "เคล็ดลับ 1 - แก้ไขแท็ก",
+ "usageTips.tip1.detail": "เปิดการตั้งค่าแล้วไปที่แท็กเพื่อแก้ไข, ดับเบิลคลิกแท็กในรายการแท็กเพื่อเปิดการแก้ไขแท็กอย่างรวดเร็ว",
"usageTips.tip2.title": "เคล็ดลับ 2 - ติดแท็กแอป",
"usageTips.tip2.detail": "กดแอปค้างไว้, วางบนแท็ก",
"usageTips.tip3.title": "เคล็ดลับ 3 - ย้ายแอป",
diff --git a/src/Apptag/Localization/tr.json b/src/Apptag/Localization/tr.json
index dfd4535..8e58f5f 100644
--- a/src/Apptag/Localization/tr.json
+++ b/src/Apptag/Localization/tr.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Etkinleştirildiğinde uygulama ızgarasının altındaki yüzen ipuçları gizlenir.",
"usageTips.previous": "Önceki ipucu",
"usageTips.next": "Sonraki ipucu",
- "usageTips.tip1.title": "İpucu 1 - Etiket ekle",
- "usageTips.tip1.detail": "Ayarları açın, etiketleri düzenleyin.",
+ "usageTips.close": "İpuçlarını kapat",
+ "usageTips.tip1.title": "İpucu 1 - Etiketleri düzenle",
+ "usageTips.tip1.detail": "Ayarları açın ve düzenlemek için Etiketler’e geçin, Etiket listesindeki bir etikete çift tıklayarak düzenlemeyi hızlı açın.",
"usageTips.tip2.title": "İpucu 2 - Uygulamaları etiketle",
"usageTips.tip2.detail": "Bir uygulamaya uzun basın, etikete bırakın.",
"usageTips.tip3.title": "İpucu 3 - Uygulamaları taşı",
diff --git a/src/Apptag/Localization/uk.json b/src/Apptag/Localization/uk.json
index cbcbc9d..54d9cfb 100644
--- a/src/Apptag/Localization/uk.json
+++ b/src/Apptag/Localization/uk.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Якщо ввімкнено, плаваючі поради внизу сітки програм буде приховано.",
"usageTips.previous": "Попередня порада",
"usageTips.next": "Наступна порада",
- "usageTips.tip1.title": "Порада 1 — Додати теги",
- "usageTips.tip1.detail": "Відкрийте Параметри, редагуйте теги.",
+ "usageTips.close": "Закрити поради",
+ "usageTips.tip1.title": "Порада 1 — Редагувати теги",
+ "usageTips.tip1.detail": "Відкрийте Параметри й перейдіть до Тегів для редагування, Двічі клацніть тег у списку щоб швидко відкрити редагування.",
"usageTips.tip2.title": "Порада 2 — Позначити програми",
"usageTips.tip2.detail": "Утримуйте програму, перетягніть її на тег.",
"usageTips.tip3.title": "Порада 3 — Перемістити програми",
diff --git a/src/Apptag/Localization/vi.json b/src/Apptag/Localization/vi.json
index 6914e90..77551f2 100644
--- a/src/Apptag/Localization/vi.json
+++ b/src/Apptag/Localization/vi.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "Khi bật, các mẹo nổi ở cuối lưới ứng dụng sẽ bị ẩn.",
"usageTips.previous": "Mẹo trước",
"usageTips.next": "Mẹo tiếp theo",
- "usageTips.tip1.title": "Mẹo 1 - Thêm thẻ",
- "usageTips.tip1.detail": "Mở Cài đặt, chỉnh sửa thẻ.",
+ "usageTips.close": "Đóng mẹo",
+ "usageTips.tip1.title": "Mẹo 1 - Sửa thẻ",
+ "usageTips.tip1.detail": "Mở Cài đặt và chuyển sang Thẻ để chỉnh sửa, Nhấp đúp một thẻ trong danh sách để mở nhanh phần chỉnh sửa.",
"usageTips.tip2.title": "Mẹo 2 - Gắn thẻ ứng dụng",
"usageTips.tip2.detail": "Nhấn giữ ứng dụng, thả lên thẻ.",
"usageTips.tip3.title": "Mẹo 3 - Di chuyển ứng dụng",
diff --git a/src/Apptag/Localization/zh-Hans.json b/src/Apptag/Localization/zh-Hans.json
index db1bfa2..c4cc7e3 100644
--- a/src/Apptag/Localization/zh-Hans.json
+++ b/src/Apptag/Localization/zh-Hans.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "开启后,应用网格底部的悬浮使用技巧将不再显示。",
"usageTips.previous": "上一条技巧",
"usageTips.next": "下一条技巧",
- "usageTips.tip1.title": "技巧1-添加标签",
- "usageTips.tip1.detail": "在设置 > 切换到“标签”即可编辑",
+ "usageTips.close": "关闭使用技巧",
+ "usageTips.tip1.title": "技巧1-编辑标签",
+ "usageTips.tip1.detail": "打开设置,切换到“标签”即可编辑\n在标签列表,双击标签即可快速进入标签编辑页面",
"usageTips.tip2.title": "技巧2-打标签",
"usageTips.tip2.detail": "长按应用图标启动拖动 > 拖动到标签上是打标签",
"usageTips.tip3.title": "技巧3-移动应用",
diff --git a/src/Apptag/Localization/zh-Hant.json b/src/Apptag/Localization/zh-Hant.json
index dba0c49..316288e 100644
--- a/src/Apptag/Localization/zh-Hant.json
+++ b/src/Apptag/Localization/zh-Hant.json
@@ -215,8 +215,9 @@
"settings.hideUsageTipsDesc": "開啟後,應用程式網格底部的浮動使用技巧將不再顯示。",
"usageTips.previous": "上一則技巧",
"usageTips.next": "下一則技巧",
- "usageTips.tip1.title": "技巧1-新增標籤",
- "usageTips.tip1.detail": "在設定 > 切換到「標籤」即可編輯",
+ "usageTips.close": "關閉使用技巧",
+ "usageTips.tip1.title": "技巧1-編輯標籤",
+ "usageTips.tip1.detail": "打開設定,切換到「標籤」即可編輯\n在標籤列表,雙擊標籤即可快速進入標籤編輯頁面",
"usageTips.tip2.title": "技巧2-套用標籤",
"usageTips.tip2.detail": "長按應用程式圖示開始拖曳 > 拖到標籤上即可套用標籤",
"usageTips.tip3.title": "技巧3-移動應用",
diff --git a/src/CHANGELOG.md b/src/CHANGELOG.md
index 31ae0c8..1a2befb 100644
--- a/src/CHANGELOG.md
+++ b/src/CHANGELOG.md
@@ -1,5 +1,16 @@
# TagLauncher Changelog
+## [8.0.2] — 2026-06-24
+
+- 重设计 App Grid 底部使用技巧:改为原生 AppKit 分区式教学横幅,底部横向占满可用宽度,左侧为技巧编号与标题,中央为两行动作说明,右侧为上一条/下一条控制与页点
+- 使用技巧横幅接入主题玻璃 token:默认、粉色、紫色、绿色、蓝色、炫彩等亮色主题使用浅玻璃配色;深蓝和黑色主题使用深色玻璃材质与白色文字,保证可读性
+- 优化使用技巧横幅标题区:标题字号加大并加粗,标题和图标按当前主题使用高对比 accent;每条技巧使用不同语义图标,正文多段说明显示为编号步骤
+- 更新第 1 条使用技巧为“编辑标签”:说明可在设置页切换到“标签”编辑,也可在标签列表双击标签快速进入标签编辑页;同步补齐 29 个语种文案
+- 新增低视觉权重关闭按钮:默认半透明,hover 后更清晰;点击后持久关闭使用技巧,与设置页“隐藏使用技巧”一致
+- 保持使用技巧区域的全宽事件拦截:点击横幅和其底部事件区域不会穿透到底层 App Grid,也不会误触发应用气泡或打开应用;只有命中箭头区域才切换技巧
+- 补齐 29 个语种的 `usageTips.close` 文案,并更新 `Scripts/usage_tips_qa.sh`,覆盖分区布局、亮/暗主题配色、关闭动作、事件拦截、macOS 14 兼容和 29 语种文案完整性
+- 版本号更新为 `8.0.2`,Build 更新为 `20260624.2341`
+
## [8.0.1] — 2026-06-24
- 新增 App Grid 主题系统:设置页新增“主题”页签,可在默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题之间切换
diff --git a/src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md b/src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md
index bc0ce62..43b1836 100644
--- a/src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md
+++ b/src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md
@@ -29,6 +29,12 @@
- [x] 移除旧“启用深色视图”设置入口和本地化文案。
- [x] 新增主题设置 QA 脚本。
- [x] 更新 changelog、TODO 和工作日志。
+- [x] 使用技巧横幅重设计:按分区式教学横幅落地,亮色主题用浅玻璃,深蓝/黑色主题用深玻璃和白字。
+- [x] 使用技巧新增弱视觉关闭按钮,hover 后清晰,关闭后持久写入 `hideUsageTips`。
+- [x] 使用技巧 QA 增加分区布局、主题配色、关闭动作、全宽事件拦截和 29 语种 `usageTips.close` 检查。
+- [x] 使用技巧标题区视觉优化:标题放大加粗、按主题使用高对比 accent、按技巧类型切换语义 icon。
+- [x] 使用技巧正文多段说明改为编号步骤;第 1 条改为“编辑标签”,并补充双击标签快速进入标签编辑页。
+- [x] 同步更新第 1 条使用技巧 29 个语种文案,并把标题字号、语义 icon、编号步骤和双击标签说明纳入 `usage_tips_qa.sh`。
- [ ] 用户视觉体验验收。
- [ ] 通过验收后再决定是否冻结、打包、创建正式 release tag。
diff --git a/src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md b/src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md
index c0cb414..8723f00 100644
--- a/src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md
+++ b/src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md
@@ -193,3 +193,64 @@
- `src/build/TagLauncher-8.0.0-build20260624.1815.dmg`
- SHA256:
- `373355c87055680cb13e873bc5909abd84817d9ac2eaa98cc1ec2748be64bc80`
+
+### 2026-06-24 23:41 使用技巧横幅重设计
+
+- 用户确认最终方案:
+ - 布局选“2 分区式教学横幅”。
+ - 亮色主题使用浅玻璃配色。
+ - 深色/黑色主题使用同样布局,但切换到深色玻璃材质和白色文字。
+- 角色分工结论:
+ - 架构:继续使用原生 AppKit `AppGridUsageTipsNSView`,不引入 SwiftUI;tips 跟随 `renderedAppGridTheme`,编辑模式仍继承默认浅色主题 override。
+ - 代码审核:保留全宽透明 shield 和 local mouse monitor,避免点击穿透到底层 AppGrid;箭头只在命中区域触发翻页。
+ - QA:更新 `usage_tips_qa.sh`,覆盖分区式布局、主题配色、关闭按钮、全宽事件拦截、29 语种文案和 macOS 14 兼容。
+- 本轮实现:
+ - 使用技巧横幅底部横向占满可用宽度。
+ - 左侧独立标题面板显示灯泡图标、技巧编号和标题。
+ - 中央正文区域展示两行动作说明,保留长文本横向滚动能力。
+ - 右侧固定上一条/下一条按钮,页点移动到箭头下方。
+ - 右上角新增弱视觉关闭按钮;默认半透明,hover/press 更清晰;点击后写入 `hideUsageTips`。
+ - `updateColors()` 按 `AppGridTheme.usesDarkGlass` 切换浅玻璃/深玻璃 token。
+ - 29 个语种新增 `usageTips.close`。
+- QA:
+ - `bash Scripts/usage_tips_qa.sh`:PASS。
+ - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
+ - `git diff --check`:PASS。
+ - `bash build.sh`:PASS,生成 `src/build/TagLauncher.app`。
+ - `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。
+ - `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。
+ - 构建产物版本:`8.0.2 (20260625.1322)`。
+ - `bash Scripts/theme_settings_qa.sh`:PASS。
+ - `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。
+ - `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
+ - `APP_BUILD=20260624.2331 bash build.sh`:PASS。
+
+### 2026-06-25 使用技巧横幅 26.0625-2 视觉优化
+
+- 需求来源:`canvas/26.06-UI调整.excalidraw` frame `26.0625-2`。
+- 用户标注问题:
+ - 标题区空间充裕但排版仍显拥挤。
+ - 标题字号偏小,需要适当放大并加粗,同时不能撑破 29 语种矩形空间。
+ - 标题字色需要按主题优化,必须保持清晰可读。
+ - 标题前 icon 不应固定为单一灯泡,需要按技巧类型变化。
+ - 正文超过一行时应采用 ordered list 样式。
+ - 第 1 条标题改为“编辑标签”,正文补充“打开设置切换到标签编辑”和“在标签列表双击标签快速进入标签编辑页”。
+ - 以上文案修改覆盖 29 个语种。
+- 本轮实现:
+ - `AppGridUsageTipsNSView` 标题字号从 22 semibold 调整为 25 bold。
+ - 左侧标题面板宽度从 300-380pt 扩展为 350-460pt,降低长语种标题换行/挤压风险。
+ - 标题和 icon 改为按 `AppGridTheme` 取高对比 accent;深蓝/黑色主题继续使用白字优先。
+ - 按 tip id 切换语义 SF Symbol:编辑标签、套用标签、移动、复制、移除、排序、备注等不再共用灯泡 icon。
+ - 正文 formatter 保留分隔符转换规则,并在多段内容时自动渲染为 `1.` / `2.` 编号步骤。
+ - 第 1 条使用技巧 29 个语种 title/detail 已同步更新。
+ - `Scripts/usage_tips_qa.sh` 增加标题字号、标题面板宽度、语义 icon、主题 accent、ordered list 和第 1 条双击标签说明检查。
+- QA:
+ - `bash Scripts/usage_tips_qa.sh`:PASS。
+ - `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。
+ - `bash Scripts/appgrid_startup_loading_qa.sh`:PASS。
+ - `bash Scripts/app_ordering_data_qa.sh`:PASS。
+ - `bash Scripts/apple_default_note_policy_qa.sh`:PASS。
+ - `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。
+ - `bash Scripts/quick_search_app_name_qa.sh`:SKIP,本机没有 `/Applications/贝锐向日葵被控.app` fixture。
+ - `bash Scripts/window_logic_qa.sh`:未作为本轮通过项;脚本在 Dock tile 去重检查处失败,退出后无 TagLauncher 进程残留,判断为 GUI/Dock 可访问性环境或既有 QA 基础设施问题,和本轮使用技巧横幅代码路径无直接交集。
+- 版本推进到:`8.0.2 / 20260624.2341`。
diff --git a/src/Scripts/usage_tips_qa.sh b/src/Scripts/usage_tips_qa.sh
index e663d6f..25008cb 100755
--- a/src/Scripts/usage_tips_qa.sh
+++ b/src/Scripts/usage_tips_qa.sh
@@ -7,6 +7,7 @@
APPTAG_APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift"
PREFERENCES_VIEW_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
APP_DEFAULTS_SWIFT="$ROOT_DIR/Apptag/AppDefaults.swift"
+APP_GRID_THEME_SWIFT="$ROOT_DIR/Apptag/AppGridTheme.swift"
LOCALIZATION_DIR="$ROOT_DIR/Apptag/Localization"
fail() {
@@ -19,9 +20,10 @@
[[ -f "$APPTAG_APP_SWIFT" ]] || fail "missing ApptagApp.swift"
[[ -f "$PREFERENCES_VIEW_SWIFT" ]] || fail "missing PreferencesView.swift"
[[ -f "$APP_DEFAULTS_SWIFT" ]] || fail "missing AppDefaults.swift"
+[[ -f "$APP_GRID_THEME_SWIFT" ]] || fail "missing AppGridTheme.swift"
[[ -d "$LOCALIZATION_DIR" ]] || fail "missing Localization directory"
-python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$LOCALIZATION_DIR" <<'PY'
+python3 - "$CONTENT_VIEW_SWIFT" "$APP_GRID_SWIFT" "$APPTAG_APP_SWIFT" "$PREFERENCES_VIEW_SWIFT" "$APP_DEFAULTS_SWIFT" "$APP_GRID_THEME_SWIFT" "$LOCALIZATION_DIR" <<'PY'
import json
import pathlib
import re
@@ -32,17 +34,8 @@
apptag_app = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8")
preferences = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")
defaults = pathlib.Path(sys.argv[5]).read_text(encoding="utf-8")
-localization_dir = pathlib.Path(sys.argv[6])
-
-required_keys = [
- "settings.hideUsageTips",
- "settings.hideUsageTipsDesc",
- "usageTips.previous",
- "usageTips.next",
-]
-for index in range(1, 9):
- required_keys.append(f"usageTips.tip{index}.title")
- required_keys.append(f"usageTips.tip{index}.detail")
+theme = pathlib.Path(sys.argv[6]).read_text(encoding="utf-8")
+localization_dir = pathlib.Path(sys.argv[7])
def fail(message: str) -> None:
@@ -55,6 +48,17 @@
fail(message)
+required_keys = [
+ "settings.hideUsageTips",
+ "settings.hideUsageTipsDesc",
+ "usageTips.previous",
+ "usageTips.next",
+ "usageTips.close",
+]
+for index in range(1, 9):
+ required_keys.append(f"usageTips.tip{index}.title")
+ required_keys.append(f"usageTips.tip{index}.detail")
+
require(
r"static\s+let\s+hideUsageTips\s*=\s*false",
defaults,
@@ -65,121 +69,34 @@
defaults,
"AppDefaults.register must register hideUsageTips",
)
-require(
- r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips',
- content_view,
- "ContentView must observe hideUsageTips with AppStorage",
-)
-require(
- r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips',
- preferences,
- "PreferencesView must persist hideUsageTips with AppStorage",
-)
+for source_name, source in {
+ "ContentView.swift": content_view,
+ "PreferencesView.swift": preferences,
+}.items():
+ require(
+ r'@AppStorage\("hideUsageTips"\)\s+private\s+var\s+hideUsageTips\s*=\s*AppDefaults\.hideUsageTips',
+ source,
+ f"{source_name} must persist hideUsageTips with AppStorage",
+ )
+
require(
r'Toggle\s*\(\s*tr\("settings\.hideUsageTips"\)\s*,\s*isOn\s*:\s*\$hideUsageTips\s*\)',
preferences,
"General settings must expose a Hide usage tips toggle",
)
+
require(
r"\bprivate\s+final\s+class\s+AppGridUsageTipsNSView\s*:\s*NSView\b",
app_grid,
"Usage tips must be implemented as a native AppKit NSView",
)
+if re.search(r"AppGridUsageTipsBar\s*:\s*View|usageTipNavigationButton|Text\s*\(\s*tr\s*\(\s*tip\.", content_view):
+ fail("usage tips must not be rendered by SwiftUI in ContentView")
+
require(
- r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*136(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*176",
+ r"enum\s+AppGridUsageTipsMetrics\s*\{(?P<body>.*?)static\s+let\s+barHeight\s*:\s*CGFloat\s*=\s*154(?P<body2>.*?)static\s+let\s+reservedHeight\s*:\s*CGFloat\s*=\s*194",
app_grid,
- "Usage tips overlay must reserve a design-reviewed multi-line bottom space for the native HUD bar",
-)
-require(
- r"NSVisualEffectView\s*\(",
- app_grid,
- "Usage tips must use a native macOS visual effect surface instead of split black/white blocks",
-)
-require(
- r'systemSymbolName\s*:\s*"lightbulb\.fill"',
- app_grid,
- "Usage tips HUD must include a compact native guidance icon",
-)
-require(
- r"preferredWidth\s*\(\s*maxAvailableWidth\s*:\s*CGFloat\s*\)",
- app_grid,
- "Usage tips HUD must be content-adaptive instead of stretched across the full AppGrid",
-)
-if re.search(r"static\s+let\s+maxWidth\s*:\s*CGFloat", app_grid):
- fail("Usage tips HUD must not use a hard maximum width that truncates localized titles")
-require(
- r"let\s+titleWidth\s*=\s*ceil\s*\(\s*titleLabel\.attributedStringValue\.size\(\)\.width\s*\)",
- app_grid,
- "Usage tips layout must measure the localized title width instead of using a fixed title column",
-)
-require(
- r"configureLabel\s*\(\s*titleLabel\s*,\s*font\s*:\s*titleFont\s*,\s*lineBreakMode\s*:\s*\.byClipping\s*\)",
- app_grid,
- "Usage tip titles must not use tail truncation ellipses",
-)
-require(
- r"let\s+titleWidth\s*=\s*availableTextWidth",
- app_grid,
- "Usage tip title label must span the text column so centered titles are visually centered",
-)
-require(
- r"configureLabel\s*\(_\s+label\s*:\s*NSTextField,\s*font\s*:\s*NSFont,\s*lineBreakMode\s*:\s*NSLineBreakMode\s*\)\s*\{(?P<body>.*?)label\.alignment\s*=\s*\.center",
- app_grid,
- "Usage tip title labels must be horizontally centered",
-)
-if re.search(r"configureLabel\s*\(\s*titleLabel\s*,\s*font\s*:\s*titleFont\s*,\s*lineBreakMode\s*:\s*\.byTruncatingTail\s*\)", app_grid):
- fail("Usage tip titles must not be configured with byTruncatingTail")
-require(
- r"relayoutUsageTipsAfterContentChange\s*\(\s*\)",
- app_grid,
- "Usage tips must ask the host to recompute width after localized text changes",
-)
-require(
- r"controlZoneWidth\s*:\s*CGFloat\s*=\s*176",
- app_grid,
- "Usage tips layout must reserve a fixed right-side control zone",
-)
-require(
- r"availableTextWidth\s*=\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*titleX\s*\)",
- app_grid,
- "Usage tips layout must allocate text from actual available AppGrid width",
-)
-require(
- r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap",
- app_grid,
- "Usage tips page dots must sit centered below the previous/next arrow buttons",
-)
-require(
- r"detailRight\s*=\s*textRight",
- app_grid,
- "Usage tips detail text must stop before the fixed right-side arrow control zone",
-)
-require(
- r"configureDetailLabel\s*\(\s*\)(?P<body>.*?)detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping",
- app_grid,
- "Usage tip detail text must wrap naturally inside the reserved text column",
-)
-require(
- r"detailLabel\.maximumNumberOfLines\s*=\s*2",
- app_grid,
- "Usage tip detail text must use the design-reviewed two-line body layout",
-)
-require(
- r"return\s+min\s*\(\s*maxAvailableWidth\s*,\s*preferredWidth\s*\)",
- app_grid,
- "Usage tips preferred width must expand up to available AppGrid width for long localizations",
-)
-require(
- r"detailScrollView\.hasHorizontalScroller\s*=\s*true",
- app_grid,
- "Long localized tip text must be horizontally scrollable in the native AppKit bar",
-)
-if len(re.findall(r"NSFont\.systemFont\s*\(\s*ofSize\s*:\s*24\s*,\s*weight\s*:\s*\.(?:semibold|regular)\s*\)", app_grid)) < 2:
- fail("Usage tip title/detail fonts must use readable 24pt native HUD typography")
-require(
- r"override\s+func\s+scrollWheel\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{\s*detailScrollView\.scrollWheel\s*\(\s*with\s*:\s*event\s*\)",
- app_grid,
- "Usage tips must route wheel events to the native horizontal text scroller",
+ "usage tips must reserve the new design-reviewed full-width teaching banner height",
)
require(
r"bottomContentPadding\s*:\s*shouldShowUsageTips\s*\?\s*AppGridUsageTipsMetrics\.reservedHeight\s*:\s*0",
@@ -189,257 +106,222 @@
require(
r"usageTipsVisible\s*:\s*shouldShowUsageTips",
content_view,
- "ContentView must pass visibility into the native AppKit usage tips bar",
+ "ContentView must pass usage tip visibility into AppGrid",
)
require(
r"selectedUsageTipIndex\s*:\s*\$selectedUsageTipIndex",
content_view,
- "ContentView must bind usage tip selection to the native AppKit bar",
+ "ContentView must bind usage tip selection to AppGrid",
)
require(
- r"override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{(?P<body>.*?)bounds\.contains\s*\(\s*point\s*\)",
+ r"onHideUsageTips\s*:\s*\{\s*hideUsageTips\s*=\s*true\s*\}",
+ content_view,
+ "closing the native usage tips banner must persist hideUsageTips",
+)
+require(
+ r"let\s+onHideUsageTips\s*:\s*\(\)\s*->\s*Void",
app_grid,
- "Native usage tips bar must own hit testing so clicks do not fall through to AppGrid",
+ "AppGridCollectionView must accept an onHideUsageTips callback",
)
require(
- r"override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{(?P<body>.*?)usageTipsView\.frame\.contains\s*\(\s*point\s*\)",
+ r"closeButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.hideUsageTips\(\)\s*\}",
app_grid,
- "AppGrid host must prioritize the native usage tips hit-test region above the collection view",
+ "close button must call the native hideUsageTips handler",
)
require(
+ r"coordinator\?\.onHideUsageTips\(\)",
+ app_grid,
+ "native hideUsageTips must call back to persisted SwiftUI state",
+)
+
+require(
+ r"private\s+let\s+titlePanelView\s*=\s*NSView\(\)",
+ app_grid,
+ "usage tips must use a separated title panel",
+)
+require(
+ r"private\s+let\s+titleFont\s*=\s*NSFont\.systemFont\s*\(\s*ofSize\s*:\s*25\s*,\s*weight\s*:\s*\.bold\s*\)",
+ app_grid,
+ "usage tips title must use the design-reviewed larger bold title font",
+)
+require(
+ r"titlePanelWidth\s*=\s*min\s*\((?P<body>.*?)max\s*\(\s*350\s*,\s*visualFrame\.width\s*\*\s*0\.24\s*\)(?P<body2>.*?)min\s*\(\s*460\s*,\s*visualFrame\.width\s*\*\s*0\.36\s*\)",
+ app_grid,
+ "usage tips title panel must be wide enough for 29-language titles",
+)
+require(
+ r"private\s+let\s+closeButton\s*=\s*AppGridUsageTipIconButton\s*\(\s*systemImage\s*:\s*\"xmark\"\s*\)",
+ app_grid,
+ "usage tips must provide a low-emphasis native close button",
+)
+require(
+ r"controlZoneWidth\s*:\s*CGFloat\s*=\s*min\s*\(\s*148\s*,\s*max\s*\(\s*128\s*,\s*visualFrame\.width\s*\*\s*0\.08\s*\)\s*\)",
+ app_grid,
+ "usage tips must reserve a stable 128-148pt right-side control zone",
+)
+require(
+ r"func\s+preferredWidth\s*\(\s*maxAvailableWidth\s*:\s*CGFloat\s*\)\s*->\s*CGFloat\s*\{\s*max\s*\(\s*1\s*,\s*maxAvailableWidth\s*\)\s*\}",
+ app_grid,
+ "usage tips visual banner must occupy the available bottom width",
+)
+require(
+ r"dotsView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*buttonsGroupMinX\s*\+\s*\(buttonsWidth\s*-\s*dotsWidth\)\s*/\s*2(?P<body2>.*?)y\s*:\s*previousButton\.frame\.maxY\s*\+\s*buttonDotsGap",
+ app_grid,
+ "page dots must be centered below the previous/next arrows",
+)
+require(
+ r"detailScrollView\.frame\s*=\s*NSRect\s*\((?P<body>.*?)x\s*:\s*detailX(?P<body2>.*?)width\s*:\s*max\s*\(\s*1\s*,\s*textRight\s*-\s*detailX\s*\)",
+ app_grid,
+ "detail text must live in the middle reading area and stop before controls",
+)
+require(
+ r"detailLabel\.lineBreakMode\s*=\s*\.byWordWrapping",
+ app_grid,
+ "detail text must wrap inside the two-line teaching banner",
+)
+require(
+ r"detailLabel\.maximumNumberOfLines\s*=\s*2",
+ app_grid,
+ "detail text must keep the design-reviewed two-line layout",
+)
+require(
+ r"detailLabel\.cell\s*=\s*AppGridCenteredMultilineTextFieldCell\s*\(\s*textCell\s*:\s*\"\"\s*\)",
+ app_grid,
+ "detail text must use a vertically centered multiline cell",
+)
+require(
+ r"paragraphStyle\.minimumLineHeight\s*=\s*31(?P<body>.*?)paragraphStyle\.maximumLineHeight\s*=\s*34(?P<body2>.*?)paragraphStyle\.lineSpacing\s*=\s*6",
+ app_grid,
+ "detail text must use the design-reviewed line height and spacing",
+)
+require(
+ r"detailScrollView\.hasHorizontalScroller\s*=\s*true",
+ app_grid,
+ "long localized tip text must remain horizontally scrollable",
+)
+require(
+ r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)",
+ app_grid,
+ "usage tips detail text must normalize separators for display",
+)
+require(
+ r"orderedTipDetail\s*\(_\s+text\s*:\s*String\s*\)(?P<body>.*?)map\s*\{\s*\"\\\(\$0\.offset\s*\+\s*1\)\.\s*\\\(\$0\.element\)\"\s*\}",
+ app_grid,
+ "multi-line usage tips detail must be rendered as an ordered list",
+)
+require(
+ r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\|,\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"",
+ app_grid,
+ "usage tips detail text must turn separators into hard line breaks",
+)
+require(
+ r"updateTipIcon\s*\(\s*id\s*:\s*tip\.id\s*\)",
+ app_grid,
+ "usage tips must update the leading icon for each tip",
+)
+require(
+ r"private\s+func\s+updateTipIcon\s*\(\s*id\s*:\s*Int\s*\)(?P<body>.*?)case\s+1:(?P<body2>.*?)tag\.fill(?P<body3>.*?)case\s+8:(?P<body4>.*?)note\.text",
+ app_grid,
+ "usage tips must vary the leading icon by tip type",
+)
+
+require(
+ r"private\s+func\s+updateColors\(\)(?P<body>.*?)coordinator\?\.appGridTheme\.usesDarkGlass\s*==\s*true",
+ app_grid,
+ "usage tips colors must be driven by the current AppGrid theme",
+)
+for token in [
+ r"backgroundView\.material\s*=\s*usesDarkGlass\s*\?\s*\.underWindowBackground\s*:\s*\.popover",
+ r"backgroundView\.appearance\s*=\s*usesDarkGlass\s*\?\s*NSAppearance\s*\(\s*named\s*:\s*\.darkAqua\s*\)\s*:\s*nil",
+ r"titlePanelView\.layer\?\.backgroundColor",
+ r"usageTipAccentColor\s*\(\s*for\s*:\s*theme\s*\)",
+ r"titleLabel\.textColor\s*=\s*usesDarkGlass",
+ r"detailLabel\.textColor\s*=\s*usesDarkGlass",
+ r"closeButton\.hoveredTintColor\s*=\s*usesDarkGlass",
+ r"dotsView\.selectedColor\s*=\s*usesDarkGlass",
+]:
+ require(token, app_grid, f"usage tips theme token missing: {token}")
+require(
+ r"private\s+func\s+usageTipAccentColor\s*\(\s*for\s+theme\s*:\s*AppGridTheme\s*\)\s*->\s*NSColor(?P<body>.*?)case\s+\.pink:(?P<body2>.*?)case\s+\.colorful:",
+ app_grid,
+ "usage tips title color must provide per-theme readable accent colors",
+)
+
+require(
+ r"var\s+usesDarkGlass\s*:\s*Bool\s*\{(?P<body>.*?)case\s+\.deepBlue,\s*\.black:(?P<body2>.*?)return\s+true",
+ theme,
+ "only dark/deep-black themes should use the dark glass palette",
+)
+
+for token in [
r"usageTipsEventRegion\s*\(\s*\)\s*->\s*NSRect",
- app_grid,
- "AppGrid host must reserve a full bottom event region for tips so clicks cannot pass through",
-)
-require(
- r"private\s+final\s+class\s+AppGridUsageTipsShieldView\s*:\s*NSView\b",
- app_grid,
- "AppGrid must use a native transparent shield view for the full bottom tips event region",
-)
-require(
r"usageTipsShieldView\.frame\s*=\s*usageTipsEventRegion\s*\(\s*\)",
- app_grid,
- "Usage tips shield must cover the full bottom event region, not just the visible HUD",
-)
-require(
r"usageTipsView\.frame\s*=\s*usageTipsEventRegion\s*\(\s*\)",
- app_grid,
- "Usage tips HUD hit-test view itself must cover the full bottom event region",
-)
-require(
- r"configureVisualLayout\s*\(\s*width\s*:\s*CGFloat\s*,\s*height\s*:\s*CGFloat\s*,\s*bottomMargin\s*:\s*CGFloat\s*\)",
- app_grid,
- "Usage tips must separate full hit-test coverage from the centered visual HUD frame",
-)
-require(
- r"currentVisualFrame\s*\(\s*\)\s*->\s*NSRect",
- app_grid,
- "Usage tips must compute a centered visual frame inside the full bottom hit-test region",
-)
-require(
r"addSubview\s*\(\s*usageTipsShieldView\s*,\s*positioned\s*:\s*\.above\s*,\s*relativeTo\s*:\s*scrollView\s*\)",
- app_grid,
- "Usage tips shield must sit above the AppGrid collection scroll view",
-)
-require(
r"addSubview\s*\(\s*usageTipsView\s*,\s*positioned\s*:\s*\.above\s*,\s*relativeTo\s*:\s*usageTipsShieldView\s*\)",
- app_grid,
- "Visible usage tips HUD must sit above the transparent event shield",
-)
-require(
r"claimUsageTipsEventRegion\s*\(\s*\)",
- app_grid,
- "AppGrid host must claim bottom tips events and suppress lower bubbles",
-)
-require(
r"handleUsageTipsMouseDown\s*\(\s*_\s+event\s*:\s*NSEvent\s*\)\s*->\s*Bool",
- app_grid,
- "AppGrid host must expose a native AppKit event router for usage tips clicks",
-)
+]:
+ require(token, app_grid, f"usage tips click-through protection missing: {token}")
require(
r"addLocalMonitorForEvents\s*\(\s*matching\s*:\s*\[(?P<body>.*?)\.leftMouseDown(?P<body2>.*?)\.leftMouseUp(?P<body3>.*?)\.rightMouseDown(?P<body4>.*?)\.otherMouseUp",
app_grid,
"AppGrid host must intercept usage tips mouseDown/mouseUp events before NSCollectionView app items receive them",
)
require(
- r"\.leftMouseUp(?P<body>.*?)\.rightMouseUp(?P<body2>.*?)\.otherMouseUp",
- app_grid,
- "AppGrid host must also swallow mouseUp events in the usage tips region so app icons below cannot open on release",
-)
-require(
r"triggerButtons\s*:\s*event\.type\s*==\s*\.leftMouseDown",
app_grid,
- "Usage tips monitor must only trigger paging on left mouseDown, not on mouseUp or right/other clicks",
+ "usage tips monitor must only trigger page changes on left mouseDown",
)
require(
- r"usageTipsHostPoint\s*\(\s*for\s+event\s*:\s*NSEvent\s*\)\s*->\s*NSPoint",
+ r"func\s+handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)if\s+triggerButtons,\s*routeButtonClickIfNeeded\s*\(\s*event\s*\)(?P<body2>.*?)claimInteractionFocus\(\)(?P<body3>.*?)return\s+true",
app_grid,
- "Usage tips mouse monitor must resolve the event position through the AppGrid host",
+ "host-routed tips clicks must consume the event and only trigger buttons on hit regions",
)
-require(
- r"window\.convertPoint\s*\(\s*fromScreen\s*:\s*NSEvent\.mouseLocation\s*\)",
- app_grid,
- "Usage tips hit testing must fall back to current screen mouse location when event.window is unreliable",
-)
-require(
- r"self\.handleUsageTipsMouseEvent\s*\(\s*event\s*,(?P<body>.*?)triggerButtons\s*:\s*event\.type\s*==\s*\.leftMouseDown(?P<body2>.*?)return\s+nil",
- app_grid,
- "Usage tips mouse monitor must swallow handled bottom-tip events so app icons below cannot open",
-)
-require(
- r"removeUsageTipsMouseMonitor\s*\(\s*\)",
- app_grid,
- "Usage tips mouse monitor must be removed when the AppGrid host leaves its window",
-)
-require(
- r"func\s+handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)if\s+triggerButtons\s*,\s*routeButtonClickIfNeeded\s*\(\s*event\s*\)(?P<body2>.*?)claimInteractionFocus\s*\(\s*\)(?P<body3>.*?)return\s+true",
- app_grid,
- "Usage tips host-routed clicks must only trigger page changes on arrow hit regions and otherwise only consume the event",
-)
-if re.search(r"handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent\s*,\s*triggerButtons\s*:\s*Bool\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)target\.mouseDown", app_grid, re.S):
- fail("Usage tips host-routed clicks must not redispatch arbitrary mouseDown events that can pierce to app icons")
-require(
- r"override\s+func\s+mouseDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)",
- app_grid,
- "Native usage tips bar must swallow mouseDown events instead of letting clicks close the grid",
-)
-require(
+if re.search(r"handleMouseEventFromHost\s*\(\s*_\s+event\s*:\s*NSEvent,\s*triggerButtons\s*:\s*Bool\s*\).*?target\.mouseDown", app_grid, re.S):
+ fail("usage tips host-routed clicks must not redispatch arbitrary mouseDown events")
+
+for token in [
r"private\s+final\s+class\s+AppGridUsageTipIconButton\s*:\s*NSView\b",
- app_grid,
- "Usage tip navigation controls must be native AppKit hit-testable views",
-)
-require(
- r"private\s+final\s+class\s+AppGridDecorativeImageView\s*:\s*NSImageView\b(?P<body>.*?)override\s+func\s+hitTest\s*\(\s*_\s+point\s*:\s*NSPoint\s*\)\s*->\s*NSView\?\s*\{\s*nil\s*\}",
- app_grid,
- "Usage tip icon images must not steal hit testing from their parent AppKit controls",
-)
-require(
- r"private\s+final\s+class\s+AppGridUsageTipIconButton\s*:\s*NSView\b(?P<body>.*?)acceptsFirstMouse\s*\(\s*for\s+event\s*:\s*NSEvent\?\s*\)\s*->\s*Bool\s*\{\s*true\s*\}",
- app_grid,
- "Usage tip navigation controls must accept first mouse clicks in the floating overlay",
-)
-require(
+ r"override\s+func\s+acceptsFirstMouse\s*\(\s*for\s+event\s*:\s*NSEvent\?\s*\)\s*->\s*Bool\s*\{\s*true\s*\}",
r"override\s+func\s+isAccessibilityElement\s*\(\s*\)\s*->\s*Bool\s*\{\s*true\s*\}",
- app_grid,
- "Usage tip navigation controls must be exposed as accessibility elements",
-)
-require(
r"override\s+func\s+accessibilityRole\s*\(\s*\)\s*->\s*NSAccessibility\.Role\?\s*\{\s*\.button\s*\}",
- app_grid,
- "Usage tip navigation controls must expose the AX button role",
-)
-require(
r"override\s+func\s+accessibilityPerformPress\s*\(\s*\)\s*->\s*Bool\s*\{(?P<body>.*?)action\?\(\)(?P<body2>.*?)return\s+true",
- app_grid,
- "Usage tip navigation controls must support AX press actions",
-)
-require(
- r"override\s+var\s+mouseDownCanMoveWindow\s*:\s*Bool\s*\{\s*false\s*\}",
- app_grid,
- "Usage tips hit-test views must not let mouseDown move or dismiss the overlay window",
-)
-require(
- r"func\s+selectUsageTip\s*\(\s*offset\s*:\s*Int\s*\)",
- app_grid,
- "Usage tip navigation must update the selected tip index from AppKit",
-)
+ r"var\s+hoveredTintColor\s*:\s*NSColor\?",
+ r"var\s+pressedTintColor\s*:\s*NSColor\?",
+ r"func\s+performPressFeedback\(\)",
+ r"private\s+func\s+updateIconTint\(\)",
+]:
+ require(token, app_grid, f"native usage tip control behavior missing: {token}")
require(
r"previousButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.selectUsageTip\s*\(\s*offset\s*:\s*-1\s*\)\s*\}",
app_grid,
- "Previous usage tip control must call the native selection handler",
+ "previous usage tip control must page backward",
)
require(
r"nextButton\.action\s*=\s*\{\s*\[weak\s+self\]\s+in\s+self\?\.selectUsageTip\s*\(\s*offset\s*:\s*1\s*\)\s*\}",
app_grid,
- "Next usage tip control must call the native selection handler",
+ "next usage tip control must page forward",
)
require(
- r"override\s+func\s+mouseDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{(?P<body>.*?)action\?\(\)",
+ r"routeButtonClickIfNeeded\s*\(_\s+event\s*:\s*NSEvent\s*\)(?P<body>.*?)nextButton\.frame\.insetBy(?P<body2>.*?)previousButton\.frame\.insetBy",
app_grid,
- "Native usage tip icon buttons must invoke their action directly on mouseDown",
+ "usage tip arrows must only respond inside their hit regions",
)
require(
- r"onUsageTipIndexChange\s*\(\s*nextIndex\s*\)",
+ r"override\s+func\s+keyDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)(?P<body>.*?)kVK_LeftArrow(?P<body2>.*?)kVK_RightArrow",
app_grid,
- "Native usage tip selection must publish the new index back to SwiftUI state",
-)
-require(
- r"override\s+func\s+keyDown\s*\(\s*with\s+event\s*:\s*NSEvent\s*\)\s*\{(?P<body>.*?)kVK_LeftArrow(?P<body2>.*?)kVK_RightArrow",
- app_grid,
- "Focused native usage tips HUD must support left/right arrow key switching",
-)
-require(
- r"formattedTipDetail\s*\(_\s+text\s*:\s*String\s*\)",
- app_grid,
- "Usage tips detail text must normalize separators for display",
-)
-require(
- r"replacingOccurrences\s*\(\s*of\s*:\s*\"\\\\s\*\(\?:-->\|->\|>\|→\|>\|,\)\\\\s\*\"\s*,\s*with\s*:\s*\"\\n\"",
- app_grid,
- "Usage tips detail text must turn navigation markers and commas into hard line breaks",
-)
-require(
- r"setUsageTipsBubbleDisabled\s*\(\s*inside\s*\)",
- app_grid,
- "Hovering usage tips must suppress lower AppGrid bubbles",
-)
-require(
- r"onUsageTipsHoverChange\s*\(\s*inside\s*\)",
- app_grid,
- "Hovering usage tips must notify ContentView to clear already visible SwiftUI bubbles",
-)
-require(
- r"shouldSwallowUsageTipsBackdropClick\s*\(\s*at\s+location\s*:\s*NSPoint\s*\)\s*->\s*Bool",
- apptag_app,
- "DismissibleHostingView must not treat bottom usage tips clicks as backdrop taps",
-)
-require(
- r"routeUsageTipsMouseDownIfNeeded\s*\(\s*event\s*\)",
- apptag_app,
- "DismissibleHostingView must forward bottom usage tips clicks to native AppKit controls before swallowing",
-)
-require(
- r"findAppGridCollectionHost\s*\(\s*in\s+view\s*:\s*NSView\s*\)\s*->\s*AppGridCollectionHostView\?",
- apptag_app,
- "DismissibleHostingView must locate the native AppGrid host for usage tips event forwarding",
-)
-require(
- r"AppGridUsageTipsMetrics\.reservedHeight",
- apptag_app,
- "Backdrop usage tips guard must match the native AppGrid reserved height",
-)
-require(
- r'UserDefaults\.standard\.bool\s*\(\s*forKey\s*:\s*"hideUsageTips"\s*\)',
- apptag_app,
- "Backdrop usage tips guard must respect the hide usage tips setting",
-)
-require(
- r"private\s+func\s+handleUsageTipsHoverChange\s*\(\s*_\s+hovering\s*:\s*Bool\s*\)",
- content_view,
- "ContentView must clear existing app bubbles while the native usage tips bar is hovered",
-)
-if re.search(r"AppGridUsageTipsBar\s*:\s*View|usageTipNavigationButton|ScrollView\s*\(\s*\.horizontal\s*,\s*showsIndicators\s*:\s*textHovered", content_view):
- fail("usage tips must not be implemented with SwiftUI views in ContentView")
-if re.search(r"Text\s*\(\s*tr\s*\(\s*tip\.(?:titleKey|detailKey)", content_view):
- fail("usage tip title/detail rendering must not use SwiftUI Text")
-require(
- r"let\s+bottomContentPadding\s*:\s*CGFloat",
- app_grid,
- "AppGridCollectionView must accept bottom content padding",
-)
-if '"bottom=\\(Int(bottomContentPadding.rounded()))"' not in app_grid:
- fail("AppGrid layout signature must include bottom content padding")
-require(
- r"\+\s*max\s*\(\s*0\s*,\s*bottomContentPadding\s*\)",
- app_grid,
- "AppGrid layout plans must add bottom content padding to content height",
+ "focused native usage tips HUD must support left/right arrow keys",
)
-tip_ids = [int(value) for value in re.findall(r"AppGridUsageTip\s*\(\s*id\s*:\s*(\d+)", content_view)]
-if tip_ids != list(range(1, 9)):
- fail(f"usage tip IDs must be exactly 1 through 8, got {tip_ids}")
-if "usageTips.tip0" in content_view:
- fail("usage tips must not expose a zero-based tip number")
+for token in [
+ r"shouldSwallowUsageTipsBackdropClick\s*\(\s*at\s+location\s*:\s*NSPoint\s*\)\s*->\s*Bool",
+ r"routeUsageTipsMouseDownIfNeeded\s*\(\s*event\s*\)",
+ r"findAppGridCollectionHost\s*\(\s*in\s+view\s*:\s*NSView\s*\)\s*->\s*AppGridCollectionHostView\?",
+ r"AppGridUsageTipsMetrics\.reservedHeight",
+ r'UserDefaults\.standard\.bool\s*\(\s*forKey\s*:\s*"hideUsageTips"\s*\)',
+]:
+ require(token, apptag_app, f"App window backdrop usage-tips protection missing: {token}")
for source_name, source in {
"ContentView.swift": content_view,
@@ -450,6 +332,12 @@
fail(f"{source_name} must not rely on macOS 15/26 availability for usage tips")
if "scrollClipDisabled" in source:
fail(f"{source_name} must avoid newer scrollClipDisabled behavior for macOS 14 compatibility")
+
+tip_ids = [int(value) for value in re.findall(r"AppGridUsageTip\s*\(\s*id\s*:\s*(\d+)", content_view)]
+if tip_ids != list(range(1, 9)):
+ fail(f"usage tip IDs must be exactly 1 through 8, got {tip_ids}")
+if "usageTips.tip0" in content_view:
+ fail("usage tips must not expose a zero-based tip number")
json_files = sorted(localization_dir.glob("*.json"))
if len(json_files) != 29:
@@ -467,6 +355,23 @@
title = data[f"usageTips.tip{tip_index}.title"]
if re.search(r"\s*[::]\s*$", title):
fail(f"{path.name} usageTips.tip{tip_index}.title must not end with a colon")
+ tip1_title = data["usageTips.tip1.title"]
+ tip1_detail = data["usageTips.tip1.detail"]
+ if path.stem == "en":
+ if tip1_title != "Tip 1 - Edit tags":
+ fail("en.json usageTips.tip1.title must describe editing tags")
+ if "Double-click" not in tip1_detail or "tag list" not in tip1_detail:
+ fail("en.json usageTips.tip1.detail must mention double-clicking a tag in the tag list")
+ if path.stem == "zh-Hans":
+ if tip1_title != "技巧1-编辑标签":
+ fail("zh-Hans.json usageTips.tip1.title must be 编辑标签")
+ if "双击标签" not in tip1_detail or "标签列表" not in tip1_detail:
+ fail("zh-Hans.json usageTips.tip1.detail must mention double-clicking in the tag list")
+ if path.stem == "zh-Hant":
+ if tip1_title != "技巧1-編輯標籤":
+ fail("zh-Hant.json usageTips.tip1.title must be 編輯標籤")
+ if "雙擊標籤" not in tip1_detail or "標籤列表" not in tip1_detail:
+ fail("zh-Hant.json usageTips.tip1.detail must mention double-clicking in the tag list")
if path.stem not in {"zh-Hans", "zh-Hant"}:
for tip_index in range(1, 9):
detail = data[f"usageTips.tip{tip_index}.detail"]
@@ -478,5 +383,5 @@
if not first or not second:
fail(f"{path.name} usageTips.tip{tip_index}.detail must have text on both sides of the comma line break")
-print("PASS usage tips QA: overlay, hide setting, macOS 14-safe implementation, and 29-language localizations are present")
+print("PASS usage tips QA: native split teaching banner, theme-aware glass, click protection, close action, and 29-language localizations are present")
PY
--
Gitblit v1.9.3