Ariver
2026-06-25 a255131e66318bed23018f3d6798ebb1ea33676a
Freeze 8.0.2 usage tips banner
37 files modified
1192 ■■■■■ changed files
CODEGRAPH.md 8 ●●●●● patch | view | raw | blame | history
src/Apptag/AppGridCollectionView.swift 417 ●●●● patch | view | raw | blame | history
src/Apptag/ContentView.swift 1 ●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar-Najdi.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/cs.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/da.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/de.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/en.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/es.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/fr.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/id.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/it.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ja.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ko.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ms.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nb.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nl.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nn.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/no.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pl.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pt-BR.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ro.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ru.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sr-Cyrl.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sv.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/th.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/tr.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/uk.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/vi.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hans.json 5 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hant.json 5 ●●●●● patch | view | raw | blame | history
src/CHANGELOG.md 11 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md 6 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md 61 ●●●●● patch | view | raw | blame | history
src/Scripts/usage_tips_qa.sh 539 ●●●●● patch | view | raw | blame | history
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.
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(
src/Apptag/ContentView.swift
@@ -1242,6 +1242,7 @@
                    },
                    onScrollActivity: handleAppGridScrollActivity,
                    onDragModeChange: { setAppDragMode($0) },
                    onHideUsageTips: { hideUsageTips = true },
                    onUsageTipsHoverChange: handleUsageTipsHoverChange
                )
            }
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>
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 - نقل التطبيقات",
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 - نقل التطبيقات",
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",
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",
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",
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",
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",
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",
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",
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",
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 - アプリを移動",
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 - 앱 이동",
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",
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",
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",
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",
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",
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",
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",
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",
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 — Переместить приложения",
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 - Премести апликације",
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",
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 - ย้ายแอป",
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şı",
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 — Перемістити програми",
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",
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-移动应用",
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-移動應用",
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 个主题之间切换
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。
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`。
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