Ariver
2026-06-24 c1d6cfc2b59b2ce4f1ebf235397cdf43a4292996
Release 8.0.1 dark glass themes
41 files modified
7 files added
1866 ■■■■■ changed files
CODEGRAPH.md 65 ●●●●● patch | view | raw | blame | history
src/Apptag/AppDefaults.swift 12 ●●●●● patch | view | raw | blame | history
src/Apptag/AppGridCollectionView.swift 94 ●●●●● patch | view | raw | blame | history
src/Apptag/AppGridTheme.swift 274 ●●●●● patch | view | raw | blame | history
src/Apptag/AppLibraryController.swift 31 ●●●● patch | view | raw | blame | history
src/Apptag/ApptagApp.swift 24 ●●●● patch | view | raw | blame | history
src/Apptag/ContentView.swift 149 ●●●● patch | view | raw | blame | history
src/Apptag/EditModeViews.swift 85 ●●●● patch | view | raw | blame | history
src/Apptag/Info.plist 4 ●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar-Najdi.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ar.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/cs.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/da.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/de.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/en.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/es.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/fr.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/id.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/it.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ja.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ko.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ms.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nb.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nl.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/nn.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/no.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pl.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/pt-BR.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ro.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/ru.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sr-Cyrl.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/sv.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/th.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/tr.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/uk.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/vi.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hans.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/Localization/zh-Hant.json 13 ●●●●● patch | view | raw | blame | history
src/Apptag/PreferencesView.swift 125 ●●●●● patch | view | raw | blame | history
src/Apptag/TagNavigationView.swift 15 ●●●●● patch | view | raw | blame | history
src/CHANGELOG.md 14 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md 41 ●●●●● patch | view | raw | blame | history
src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md 195 ●●●●● patch | view | raw | blame | history
src/Scripts/appgrid_startup_loading_qa.sh 46 ●●●●● patch | view | raw | blame | history
src/Scripts/tag_double_click_preferences_qa.sh 84 ●●●●● patch | view | raw | blame | history
src/Scripts/theme_settings_qa.sh 141 ●●●●● patch | view | raw | blame | history
src/Scripts/window_logic_qa.sh 83 ●●●● patch | view | raw | blame | history
src/TODO.md 7 ●●●●● patch | view | raw | blame | history
CODEGRAPH.md
New file
@@ -0,0 +1,65 @@
# CODEGRAPH
## Stable Modules
- App entry and overlay state: `src/Apptag/ContentView.swift`
  - Reads user defaults for AppGrid display, theme, tag navigation, usage tips, and hotkeys.
  - Owns the AppGrid full-window background rendering.
  - Reuses the last complete AppLibrary snapshot before showing startup loading UI.
  - Calls `AppGridCollectionView` for AppKit grid rendering.
- App library snapshot assembly: `src/Apptag/AppLibraryController.swift`
  - Scans apps, reconciles tags, runs SmartStart when needed, and assembles AppGrid/Quick Search snapshots.
  - 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.
  - Should not own full-screen AppGrid theme gradients; it receives theme only for derived glass readability.
- AppGrid theme model: `src/Apptag/AppGridTheme.swift`
  - Central source for theme IDs, localization keys, preview swatches, full-screen background gradients, derived glass tone, and edit-mode contrast tokens.
  - Persistent key: `appGridThemeID`.
- Settings surface: `src/Apptag/PreferencesView.swift`
  - Custom SwiftUI settings tabs.
  - Theme tab writes only `appGridThemeID`; it must not mutate tags, app ordering, or category data.
  - Can be opened with an initial target tab from AppKit/overlay notifications.
- Defaults and migrations: `src/Apptag/AppDefaults.swift`
  - Registers first-run defaults.
  - Migrates legacy `useDarkAppGrid=true` to `appGridThemeID=deepBlue` when no new theme key exists.
- Localization: `src/Apptag/Localization/*.json`
  - 29 language JSON files.
  - New user-visible settings keys must be present in all files.
## QA Entrypoints
- `src/Scripts/theme_settings_qa.sh`
  - Verifies 8 theme cases, Theme tab, legacy dark-grid UI removal, AppGrid-only rendering boundary, edit-mode default-theme override, migration path, and 29-language keys.
- `src/Scripts/appgrid_startup_loading_qa.sh`
  - Verifies AppGrid startup loading uses last-snapshot reuse and delayed spinner instead of immediately exposing empty-state loading.
- `src/Scripts/macos14_availability_typecheck_qa.sh`
  - Typechecks for macOS 14.0 compatibility.
- `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.
- `src/Scripts/tag_navigation_hover_scroll_qa.sh`
  - Verifies tag hover scroll semantics remain guarded.
- `src/Scripts/tag_double_click_preferences_qa.sh`
  - Verifies double-clicking a tag navigation item opens Preferences on the Tags tab while preserving single-click activation, hover scroll, and long-press reorder wiring.
## Protected Behavior
- Theme changes are visual preferences and must not change tag data, app ordering, notes, SmartStart/category scheme, quick search, drag/drop behavior, or import/export data.
- AppGrid startup and repeated overlay creation must not immediately show a spinner just because a new `ContentView` starts with `allApps.isEmpty`; it should first reuse the last complete in-process snapshot and only show loading after a short delay if no snapshot is available.
- Editing mode uses a temporary runtime theme override: regardless of the stored `appGridThemeID`, editing renders as the default light glass AppGrid and restores the stored theme when edit mode exits.
- Individual AppGrid containers must remain a consistent translucent glass surface. They may use a derived light/dark glass tone for readability, but must not receive per-theme internal gradients.
- Edit mode controls must use the runtime rendered theme, not the stored theme, so editing stays visually identical to the default light AppGrid.
- Deep Blue and Black use dark glass. Bright Pink, Purple, Green, Blue, and Colorful themes use light glass to keep the theme bright and readable.
- 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.
Last updated: 2026-06-24, tag double-click opens Tags settings.
src/Apptag/AppDefaults.swift
@@ -13,9 +13,11 @@
    static let launchAtLogin = true
    static let showUncommonAppBubbles = false
    static let hideUsageTips = false
    static let appGridThemeID = AppGridTheme.defaultLight.rawValue
    static let useAppKitTagNavigation = true
    static func register() {
        migrateAppGridThemePreferenceIfNeeded()
        UserDefaults.standard.register(defaults: [
            "tagFontSize": tagFontSize,
            "iconSize": iconSize,
@@ -26,6 +28,7 @@
            "launchAtLogin": launchAtLogin,
            "showUncommonAppBubbles": showUncommonAppBubbles,
            "hideUsageTips": hideUsageTips,
            AppGridTheme.storageKey: appGridThemeID,
            "useAppKitTagNavigation": useAppKitTagNavigation,
            "skipTagRemovalDropConfirm": false,
            "skipUncategorizedDropConfirm": false,
@@ -40,6 +43,15 @@
        return UserDefaults.standard.persistentDomain(forName: domain)?[key] != nil
    }
    private static func migrateAppGridThemePreferenceIfNeeded() {
        let defaults = UserDefaults.standard
        guard !hasStoredValue(for: AppGridTheme.storageKey),
              hasStoredValue(for: "useDarkAppGrid"),
              defaults.bool(forKey: "useDarkAppGrid")
        else { return }
        defaults.set(AppGridTheme.deepBlue.rawValue, forKey: AppGridTheme.storageKey)
    }
    private static func removeShortcutCustomizationDefaults() {
        let defaults = UserDefaults.standard
        [
src/Apptag/AppGridCollectionView.swift
@@ -51,6 +51,7 @@
    let displayMode: String
    let iconSize: CGFloat
    let showNames: Bool
    let appGridTheme: AppGridTheme
    let bubbleDisabled: Bool
    let showUncommonAppBubbles: Bool
    let highlightedGroupName: String?
@@ -90,6 +91,7 @@
            displayMode: displayMode,
            iconSize: iconSize,
            showNames: showNames,
            appGridTheme: appGridTheme,
            bubbleDisabled: bubbleDisabled,
            showUncommonAppBubbles: showUncommonAppBubbles,
            highlightedGroupName: highlightedGroupName,
@@ -121,6 +123,7 @@
        var displayMode = AppDefaults.displayMode
        var iconSize: CGFloat = AppDefaults.iconSize
        var showNames = true
        var appGridTheme = AppGridTheme.fallback
        private var externalBubbleDisabled = false
        private var scrollBubbleDisabled = false
        var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
@@ -161,6 +164,7 @@
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
            appGridTheme: AppGridTheme,
            bubbleDisabled: Bool,
            showUncommonAppBubbles: Bool,
            highlightedGroupName: String?,
@@ -188,6 +192,7 @@
            self.displayMode = displayMode
            self.iconSize = iconSize
            self.showNames = showNames
            self.appGridTheme = appGridTheme
            self.externalBubbleDisabled = bubbleDisabled
            self.showUncommonAppBubbles = showUncommonAppBubbles
            self.highlightedGroupName = highlightedGroupName
@@ -218,6 +223,7 @@
                displayMode: displayMode,
                iconSize: iconSize,
                showNames: showNames,
                appGridTheme: appGridTheme,
                showUncommonAppBubbles: showUncommonAppBubbles,
                bottomContentPadding: self.bottomContentPadding,
                contentRevision: contentRevision
@@ -393,6 +399,7 @@
            displayMode: String,
            iconSize: CGFloat,
            showNames: Bool,
            appGridTheme: AppGridTheme,
            showUncommonAppBubbles: Bool,
            bottomContentPadding: CGFloat,
            contentRevision: Int
@@ -405,6 +412,7 @@
                displayMode,
                "\(Int(iconSize.rounded()))",
                showNames ? "names" : "nonames",
                "theme=\(appGridTheme.rawValue)",
                showUncommonAppBubbles ? "uncommon" : "allbubbles",
                "bottom=\(Int(bottomContentPadding.rounded()))",
                colorPart,
@@ -477,6 +485,7 @@
    func applyCoordinatorUpdate() {
        guard let coordinator else { return }
        applyAppearance()
        if coordinator.needsReload {
            coordinator.needsReload = false
            collectionView.reloadData()
@@ -765,6 +774,12 @@
            lastReportedBoundsOrigin = origin
        }
        return didScroll
    }
    private func applyAppearance() {
        let usesDarkGlass = coordinator?.appGridTheme.usesDarkGlass == true
        layer?.backgroundColor = NSColor.clear.cgColor
        collectionView.appearance = usesDarkGlass ? NSAppearance(named: .darkAqua) : nil
    }
}
@@ -1418,6 +1433,7 @@
            x += 5 + spacing
        }
    }
}
private final class AppGridContainerCollectionLayout: NSCollectionViewLayout {
@@ -1982,15 +1998,24 @@
        if displayStyle.usesCardSurface {
            let rect = bounds.insetBy(dx: 0.5, dy: 0.5)
            let path = NSBezierPath(roundedRect: rect, xRadius: 14, yRadius: 14)
            cardSurfaceColor().setFill()
            path.fill()
            if coordinator.isColoredContainerMode || isColorlessActive {
                tagColor.withAlphaComponent(0.30).setFill()
            if coordinator.appGridTheme.usesDarkGlass {
                drawDarkCardSurface(
                    path: path,
                    tagColor: tagColor,
                    isActive: isHovered || isNavigationHighlighted,
                    isTinted: coordinator.isColoredContainerMode || isColorlessActive
                )
            } else {
                cardSurfaceColor().setFill()
                path.fill()
                if coordinator.isColoredContainerMode || isColorlessActive {
                    tagColor.withAlphaComponent(0.30).setFill()
                    path.fill()
                }
                NSColor.labelColor.withAlphaComponent(0.08).setStroke()
                path.lineWidth = 1
                path.stroke()
            }
            NSColor.labelColor.withAlphaComponent(0.08).setStroke()
            path.lineWidth = 1
            path.stroke()
        }
        drawHeader(title: group.name, displayStyle: displayStyle)
@@ -2210,6 +2235,17 @@
            && (isHovered || isNavigationHighlighted)
        let shouldShadow = coordinator.displayStyle.usesCardSurface
            && ((coordinator.isColoredContainerMode && (isHovered || isNavigationHighlighted)) || isColorlessActive)
        if coordinator.appGridTheme.usesDarkGlass && coordinator.displayStyle.usesCardSurface {
            let tagColor = TagColor.nsColor(for: coordinator.tagColors[group?.name ?? ""] ?? 0)
            let active = isHovered || isNavigationHighlighted
            layer?.shadowColor = active
                ? tagColor.withAlphaComponent(0.82).cgColor
                : NSColor.black.withAlphaComponent(0.72).cgColor
            layer?.shadowOpacity = active ? 0.50 : 0.28
            layer?.shadowRadius = active ? 24 : 14
            layer?.shadowOffset = NSSize(width: 0, height: active ? -8 : -4)
            return
        }
        layer?.shadowColor = NSColor.black.cgColor
        layer?.shadowOpacity = shouldShadow ? 0.22 : 0
        layer?.shadowRadius = shouldShadow ? 8 : 0
@@ -2245,6 +2281,40 @@
        return NSColor.white.withAlphaComponent(0.62)
    }
    private func drawDarkCardSurface(
        path: NSBezierPath,
        tagColor: NSColor,
        isActive: Bool,
        isTinted: Bool
    ) {
        NSGraphicsContext.saveGraphicsState()
        if isActive {
            let glow = NSShadow()
            glow.shadowColor = tagColor.withAlphaComponent(0.36)
            glow.shadowBlurRadius = 28
            glow.shadowOffset = NSSize(width: 0, height: -8)
            glow.set()
        }
        NSColor(calibratedRed: 0.12, green: 0.18, blue: 0.25, alpha: 0.58).setFill()
        path.fill()
        NSGraphicsContext.restoreGraphicsState()
        if isTinted {
            tagColor.withAlphaComponent(isActive ? 0.18 : 0.10).setFill()
            path.fill()
        }
        NSColor.white.withAlphaComponent(isActive ? 0.08 : 0.045).setFill()
        path.fill()
        let strokeColor = isActive
            ? tagColor.withAlphaComponent(0.72)
            : NSColor.white.withAlphaComponent(0.18)
        strokeColor.setStroke()
        path.lineWidth = isActive ? 1.4 : 1.0
        path.stroke()
    }
    private func drawHeader(title: String, displayStyle: AppGridCollectionDisplayMode) {
        let horizontalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
        let verticalInset = displayStyle.usesCardSurface ? AppGridCollectionMetrics.cardPadding : 0
@@ -2254,9 +2324,13 @@
            width: max(1, bounds.width - horizontalInset * 2),
            height: AppGridCollectionMetrics.headerHeight
        )
        let isDarkGrid = coordinator?.appGridTheme.usesDarkGlass == true
        let headerTextColor = isDarkGrid
            ? NSColor.white.withAlphaComponent(0.74)
            : NSColor.secondaryLabelColor
        let attributes: [NSAttributedString.Key: Any] = [
            .font: NSFont.systemFont(ofSize: 18, weight: .semibold),
            .foregroundColor: NSColor.secondaryLabelColor,
            .foregroundColor: headerTextColor,
            .paragraphStyle: centeredParagraph(lineBreak: .byTruncatingMiddle)
        ]
        let titleSize = title.size(withAttributes: attributes)
@@ -2268,7 +2342,9 @@
            height: headerRect.height - 6
        )
        let lineY = headerRect.midY
        NSColor.secondaryLabelColor.withAlphaComponent(0.25).setStroke()
        (isDarkGrid ? NSColor.white : NSColor.secondaryLabelColor)
            .withAlphaComponent(isDarkGrid ? 0.16 : 0.25)
            .setStroke()
        let leftLine = NSBezierPath()
        leftLine.move(to: NSPoint(x: headerRect.minX, y: lineY))
        leftLine.line(to: NSPoint(x: max(headerRect.minX, titleRect.minX - 2), y: lineY))
src/Apptag/AppGridTheme.swift
New file
@@ -0,0 +1,274 @@
import SwiftUI
import AppKit
enum AppGridTheme: String, CaseIterable, Identifiable {
    case defaultLight
    case deepBlue
    case black
    case pink
    case purple
    case green
    case blue
    case colorful
    static let storageKey = "appGridThemeID"
    static let fallback = AppGridTheme.defaultLight
    var id: String { rawValue }
    init(storedID: String) {
        self = AppGridTheme(rawValue: storedID) ?? .fallback
    }
    var titleKey: String {
        switch self {
        case .defaultLight: return "theme.default"
        case .deepBlue: return "theme.deepBlue"
        case .black: return "theme.black"
        case .pink: return "theme.pink"
        case .purple: return "theme.purple"
        case .green: return "theme.green"
        case .blue: return "theme.blue"
        case .colorful: return "theme.colorful"
        }
    }
    var isDefaultLight: Bool {
        self == .defaultLight
    }
    var usesVisualEffectBackdrop: Bool {
        self != .black
    }
    var isPureBlackBackground: Bool {
        self == .black
    }
    var usesDarkGlass: Bool {
        switch self {
        case .deepBlue, .black:
            return true
        case .defaultLight, .pink, .purple, .green, .blue, .colorful:
            return false
        }
    }
    var material: NSVisualEffectView.Material {
        usesDarkGlass ? .underWindowBackground : .hudWindow
    }
    var backgroundDimmingOpacity: Double {
        switch self {
        case .deepBlue:
            return 0.20
        case .defaultLight, .black, .pink, .purple, .green, .blue, .colorful:
            return 0
        }
    }
    var editPrimaryTextColor: Color {
        usesDarkGlass ? Color.white : Color(red: 0.08, green: 0.10, blue: 0.13)
    }
    var editSecondaryTextColor: Color {
        usesDarkGlass
            ? Color.white.opacity(0.78)
            : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.78)
    }
    var editTertiaryTextColor: Color {
        usesDarkGlass
            ? Color.white.opacity(0.58)
            : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.56)
    }
    var editDividerColor: Color {
        usesDarkGlass ? Color.white.opacity(0.22) : Color.black.opacity(0.16)
    }
    var editToolbarSurfaceColor: Color {
        if isPureBlackBackground {
            return Color.black.opacity(0.82)
        }
        return usesDarkGlass ? Color.black.opacity(0.54) : Color.white.opacity(0.56)
    }
    var editControlSurfaceColor: Color {
        usesDarkGlass ? Color.white.opacity(0.18) : Color.white.opacity(0.72)
    }
    var editControlStrokeColor: Color {
        usesDarkGlass ? Color.white.opacity(0.38) : Color.black.opacity(0.18)
    }
    var editInactiveIndicatorColor: Color {
        usesDarkGlass ? Color.white.opacity(0.42) : Color.black.opacity(0.36)
    }
    var editDisabledTextColor: Color {
        usesDarkGlass
            ? Color.white.opacity(0.70)
            : Color(red: 0.10, green: 0.13, blue: 0.17).opacity(0.58)
    }
    var editDisabledSurfaceColor: Color {
        usesDarkGlass ? Color.white.opacity(0.10) : Color.black.opacity(0.07)
    }
    var editButtonShadowColor: Color {
        usesDarkGlass ? Color.black.opacity(0.28) : Color.black.opacity(0.10)
    }
    var editAccentColor: Color {
        switch self {
        case .defaultLight:
            return Color.accentColor
        case .deepBlue:
            return Color(red: 0.26, green: 0.68, blue: 1.00)
        case .black:
            return Color(red: 0.10, green: 0.52, blue: 1.00)
        case .pink:
            return Color(red: 0.92, green: 0.20, blue: 0.58)
        case .purple:
            return Color(red: 0.50, green: 0.24, blue: 0.92)
        case .green:
            return Color(red: 0.12, green: 0.62, blue: 0.24)
        case .blue:
            return Color(red: 0.08, green: 0.42, blue: 0.96)
        case .colorful:
            return Color(red: 0.00, green: 0.58, blue: 0.92)
        }
    }
    var editConfirmForegroundColor: Color {
        Color.white
    }
    var backgroundBaseColors: [Color] {
        switch self {
        case .defaultLight:
            return [
                Color.white.opacity(0.16),
                Color.white.opacity(0.08)
            ]
        case .deepBlue:
            return [
                Color(red: 0.02, green: 0.04, blue: 0.09).opacity(0.96),
                Color(red: 0.04, green: 0.09, blue: 0.18).opacity(0.94),
                Color(red: 0.02, green: 0.20, blue: 0.22).opacity(0.88)
            ]
        case .black:
            return [
                Color.black,
                Color.black
            ]
        case .pink:
            return [
                Color(red: 1.00, green: 0.86, blue: 0.94),
                Color(red: 1.00, green: 0.57, blue: 0.78),
                Color(red: 1.00, green: 0.82, blue: 0.66)
            ]
        case .purple:
            return [
                Color(red: 0.83, green: 0.75, blue: 1.00),
                Color(red: 0.58, green: 0.34, blue: 0.95),
                Color(red: 0.96, green: 0.72, blue: 1.00)
            ]
        case .green:
            return [
                Color(red: 0.80, green: 0.96, blue: 0.58),
                Color(red: 0.22, green: 0.78, blue: 0.36),
                Color(red: 0.66, green: 0.92, blue: 0.44)
            ]
        case .blue:
            return [
                Color(red: 0.70, green: 0.91, blue: 1.00),
                Color(red: 0.22, green: 0.58, blue: 1.00),
                Color(red: 0.45, green: 0.96, blue: 1.00)
            ]
        case .colorful:
            return [
                Color(red: 0.10, green: 0.78, blue: 1.00),
                Color(red: 0.34, green: 0.96, blue: 0.94),
                Color(red: 0.73, green: 0.78, blue: 1.00),
                Color(red: 1.00, green: 0.58, blue: 0.94)
            ]
        }
    }
    var backgroundAccentColors: [Color] {
        switch self {
        case .defaultLight:
            return [Color.clear, Color.clear]
        case .deepBlue:
            return [
                Color(red: 0.48, green: 0.35, blue: 0.88).opacity(0.18),
                Color.clear,
                Color(red: 0.00, green: 0.78, blue: 0.86).opacity(0.16)
            ]
        case .black:
            return [
                Color.clear,
                Color.clear
            ]
        case .pink:
            return [
                Color.white.opacity(0.28),
                Color(red: 1.00, green: 0.38, blue: 0.74).opacity(0.24),
                Color(red: 1.00, green: 0.72, blue: 0.42).opacity(0.18)
            ]
        case .purple:
            return [
                Color.white.opacity(0.20),
                Color(red: 0.43, green: 0.22, blue: 0.92).opacity(0.28),
                Color(red: 1.00, green: 0.52, blue: 0.92).opacity(0.18)
            ]
        case .green:
            return [
                Color.white.opacity(0.22),
                Color(red: 0.10, green: 0.62, blue: 0.22).opacity(0.20),
                Color(red: 0.70, green: 1.00, blue: 0.54).opacity(0.22)
            ]
        case .blue:
            return [
                Color.white.opacity(0.20),
                Color(red: 0.00, green: 0.40, blue: 1.00).opacity(0.24),
                Color(red: 0.00, green: 0.90, blue: 1.00).opacity(0.18)
            ]
        case .colorful:
            return [
                Color.white.opacity(0.16),
                Color(red: 0.00, green: 0.78, blue: 1.00).opacity(0.22),
                Color(red: 1.00, green: 0.45, blue: 0.95).opacity(0.22),
                Color(red: 0.35, green: 1.00, blue: 0.92).opacity(0.18)
            ]
        }
    }
    var previewColors: [Color] {
        switch self {
        case .defaultLight:
            return [Color.white, Color(red: 0.88, green: 0.90, blue: 0.92)]
        case .deepBlue:
            return [Color(red: 0.03, green: 0.08, blue: 0.16), Color(red: 0.02, green: 0.24, blue: 0.28)]
        case .black:
            return [Color.black, Color.black]
        case .pink:
            return [Color(red: 1.00, green: 0.86, blue: 0.94), Color(red: 1.00, green: 0.57, blue: 0.78)]
        case .purple:
            return [Color(red: 0.83, green: 0.75, blue: 1.00), Color(red: 0.58, green: 0.34, blue: 0.95)]
        case .green:
            return [Color(red: 0.80, green: 0.96, blue: 0.58), Color(red: 0.22, green: 0.78, blue: 0.36)]
        case .blue:
            return [Color(red: 0.70, green: 0.91, blue: 1.00), Color(red: 0.22, green: 0.58, blue: 1.00)]
        case .colorful:
            return [
                Color(red: 0.10, green: 0.78, blue: 1.00),
                Color(red: 0.34, green: 0.96, blue: 0.94),
                Color(red: 0.75, green: 0.82, blue: 1.00),
                Color(red: 1.00, green: 0.56, blue: 0.94)
            ]
        }
    }
}
src/Apptag/AppLibraryController.swift
@@ -29,6 +29,15 @@
}
enum AppLibraryController {
    private static let snapshotCacheLock = NSLock()
    private static var cachedSnapshot: AppLibrarySnapshot?
    static func lastSnapshot() -> AppLibrarySnapshot? {
        snapshotCacheLock.lock()
        defer { snapshotCacheLock.unlock() }
        return cachedSnapshot
    }
    static func refresh(useCache: Bool = true) -> AppLibraryRefreshResult {
        let scannedApps = AppIndexer.scan(useCache: useCache)
        let reconciledStore = TagEditor.reconcileScannedApps(scannedApps)
@@ -36,8 +45,10 @@
            apps: scannedApps,
            store: reconciledStore
        )
        let snapshot = makeSnapshot(scannedApps: scannedApps, store: smartStartResult.store)
        updateLastSnapshot(snapshot)
        return AppLibraryRefreshResult(
            snapshot: makeSnapshot(scannedApps: scannedApps, store: smartStartResult.store),
            snapshot: snapshot,
            smartStartResult: smartStartResult
        )
    }
@@ -47,8 +58,10 @@
        scannedApps: [AppInfo]
    ) -> AppLibrarySmartStartApplyResult {
        let result = SmartStartService.applySuggestion(draft)
        let snapshot = makeSnapshot(scannedApps: scannedApps, store: result.store)
        updateLastSnapshot(snapshot)
        return AppLibrarySmartStartApplyResult(
            snapshot: makeSnapshot(scannedApps: scannedApps, store: result.store),
            snapshot: snapshot,
            summary: result.summary
        )
    }
@@ -56,8 +69,10 @@
    static func applySystemInitialScheme() -> AppLibrarySystemSchemeApplyResult {
        let scannedApps = AppIndexer.scan(useCache: false)
        let result = SmartStartService.applySystemInitialScheme(apps: scannedApps)
        let snapshot = makeSnapshot(scannedApps: scannedApps, store: result.store)
        updateLastSnapshot(snapshot)
        return AppLibrarySystemSchemeApplyResult(
            snapshot: makeSnapshot(scannedApps: scannedApps, store: result.store),
            snapshot: snapshot,
            summary: result.summary
        )
    }
@@ -65,11 +80,19 @@
    static func resetToUncategorized() -> AppLibraryUncategorizedResetResult {
        let scannedApps = AppIndexer.scan(useCache: false)
        let store = TagDatabase.resetAppTagAssignmentsToUncategorized()
        let snapshot = makeSnapshot(scannedApps: scannedApps, store: store)
        updateLastSnapshot(snapshot)
        return AppLibraryUncategorizedResetResult(
            snapshot: makeSnapshot(scannedApps: scannedApps, store: store)
            snapshot: snapshot
        )
    }
    private static func updateLastSnapshot(_ snapshot: AppLibrarySnapshot) {
        snapshotCacheLock.lock()
        cachedSnapshot = snapshot
        snapshotCacheLock.unlock()
    }
    private static func makeSnapshot(
        scannedApps: [AppInfo],
        store: TagDatabase.Store
src/Apptag/ApptagApp.swift
@@ -1667,9 +1667,13 @@
            forName: .tagLauncherOpenPreferencesRequested,
            object: nil,
            queue: .main
        ) { [weak self] _ in
            self?.openPreferences()
        ) { [weak self] notification in
            self?.openPreferences(targetTab: Self.preferencesTabTarget(from: notification))
        }
    }
    private static func preferencesTabTarget(from notification: Notification) -> String? {
        notification.userInfo?[SettingsTabTarget.userInfoKey] as? String
    }
    private func observeExternalActivationRequests() {
@@ -1691,6 +1695,10 @@
    }
    @objc private func openPreferences(_ sender: Any? = nil) {
        openPreferences(targetTab: nil)
    }
    private func openPreferences(targetTab: String?) {
        dismissQuickSearchIfNeeded()
        TagDatabase.flushPendingCategorySchemeBackupBatch()
        if overlayAvoidsSpaceSwitch {
@@ -1706,6 +1714,7 @@
        if let settingsWindow {
            prepareSettingsWindow(settingsWindow)
            requestPreferencesTabSelection(targetTab)
            return
        }
@@ -1716,12 +1725,21 @@
            defer: false
        )
        window.title = tr("menu.preferences").replacingOccurrences(of: "…", with: "")
        window.contentView = NSHostingView(rootView: PreferencesView())
        window.contentView = NSHostingView(rootView: PreferencesView(initialTabRawValue: targetTab))
        window.isReleasedWhenClosed = false
        settingsWindow = window
        prepareSettingsWindow(window)
    }
    private func requestPreferencesTabSelection(_ targetTab: String?) {
        guard let targetTab else { return }
        NotificationCenter.default.post(
            name: .tagLauncherPreferencesTabRequested,
            object: nil,
            userInfo: [SettingsTabTarget.userInfoKey: targetTab]
        )
    }
    private func dismissQuickSearchIfNeeded() {
        guard isQuickSearchOpen else { return }
        isQuickSearchOpen = false
src/Apptag/ContentView.swift
@@ -8,9 +8,15 @@
    static let tagLauncherAppNoteEditingChanged = Notification.Name("TagLauncherAppNoteEditingChanged")
    static let tagLauncherDataDidChange = Notification.Name("TagLauncherDataDidChange")
    static let tagLauncherOpenPreferencesRequested = Notification.Name("TagLauncherOpenPreferencesRequested")
    static let tagLauncherPreferencesTabRequested = Notification.Name("TagLauncherPreferencesTabRequested")
    static let tagLauncherOverlayDidShow = Notification.Name("TagLauncherOverlayDidShow")
    static let tagLauncherOverlayDidHide = Notification.Name("TagLauncherOverlayDidHide")
    static let tagLauncherModalInteractionChanged = Notification.Name("TagLauncherModalInteractionChanged")
}
enum SettingsTabTarget {
    static let userInfoKey = "tab"
    static let tags = "tags"
}
// MARK: - Edit Phase
@@ -385,6 +391,8 @@
    @State private var smartStartNotice: SmartStartNotice? = nil
    @State private var pendingSmartStartDraft: SmartCategorizationDraft? = nil
    @State private var refreshInProgress = false
    @State private var loadingSpinnerVisible = false
    @State private var loadingSpinnerToken = 0
    @State private var refreshAgainAfterCurrent = false
    @State private var refreshAgainForceLayout = false
    @State private var refreshAgainUseCache = true
@@ -426,6 +434,7 @@
    @AppStorage("hideAppNames") private var hideAppNames = AppDefaults.hideAppNames
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
    @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips
    @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID
    @AppStorage("useAppKitTagNavigation") private var useAppKitTagNavigation = AppDefaults.useAppKitTagNavigation
    @AppStorage("skipTagRemovalDropConfirm") private var skipTagRemovalDropConfirm = false
    @AppStorage("skipUncategorizedDropConfirm") private var skipUncategorizedDropConfirm = false
@@ -434,6 +443,7 @@
    private let editSidebarHorizontalInset: CGFloat = 12
    private let floatingControlsTrailingInset: CGFloat = 20
    private let floatingControlsReservedWidth: CGFloat = 120
    private let loadingSpinnerDelay: TimeInterval = 0.25
    private var appBubbleDisabled: Bool {
        usageTipsHovered
            || appGridInteraction.appDragModeActive
@@ -448,9 +458,17 @@
    private let tagNavigationHoverScrollDelay: TimeInterval = 0.14
    private let tagNavigationHoverScrollInterval: TimeInterval = 0.22
    private var floatingButtonSurfaceColor: Color {
        colorScheme == .dark
        return colorScheme == .dark
            ? Color.white.opacity(0.10)
            : Color.white.opacity(0.78)
    }
    private var appGridTheme: AppGridTheme {
        AppGridTheme(storedID: appGridThemeID)
    }
    private var renderedAppGridTheme: AppGridTheme {
        editPhase == .none ? appGridTheme : .defaultLight
    }
    private var isSideLayout: Bool {
@@ -492,9 +510,7 @@
    var body: some View {
        ZStack {
            if shouldRenderAppGridBehindQuickSearch {
                VisualEffectView(material: .hudWindow, blendingMode: .behindWindow)
                    .ignoresSafeArea()
                    .allowsHitTesting(false)
                appGridBackground
            }
            if shouldRenderAppGridBehindQuickSearch {
@@ -956,6 +972,39 @@
        }
    }
    private var appGridBackground: some View {
        let theme = renderedAppGridTheme
        return ZStack {
            if theme.usesVisualEffectBackdrop {
                VisualEffectView(
                    material: theme.material,
                    blendingMode: .behindWindow
                )
            }
            if theme.isPureBlackBackground {
                Color.black
            }
            if !theme.isDefaultLight {
                LinearGradient(
                    colors: theme.backgroundBaseColors,
                    startPoint: .topLeading,
                    endPoint: .bottomTrailing
                )
                LinearGradient(
                    colors: theme.backgroundAccentColors,
                    startPoint: .topTrailing,
                    endPoint: .bottomLeading
                )
                if theme.backgroundDimmingOpacity > 0 {
                    Rectangle()
                        .fill(Color.black.opacity(theme.backgroundDimmingOpacity))
                }
            }
        }
        .ignoresSafeArea()
        .allowsHitTesting(false)
    }
    private var floatingActionButtons: some View {
        HStack(spacing: 8) {
            floatingOverlayButton(systemImage: "pencil.line") {
@@ -1053,6 +1102,9 @@
            onActivate: { tagID in
                activateTagNavigation(tagID)
            },
            onDoubleActivate: { _ in
                openTagSettingsFromNavigation()
            },
            onHoverChange: { tagID, active in
                handleTagNavigationHover(tagID, active: active)
            },
@@ -1077,6 +1129,14 @@
            onAppDrop: { path, source, targetTag, copy in
                dropAppOnTagNavigation(path: path, sourceTag: source, targetTag: targetTag, copy: copy)
            }
        )
    }
    private func openTagSettingsFromNavigation() {
        NotificationCenter.default.post(
            name: .tagLauncherOpenPreferencesRequested,
            object: nil,
            userInfo: [SettingsTabTarget.userInfoKey: SettingsTabTarget.tags]
        )
    }
@@ -1141,7 +1201,9 @@
        Group {
            if allApps.isEmpty {
                Spacer()
                ProgressView().scaleEffect(0.8)
                if loadingSpinnerVisible {
                    ProgressView().scaleEffect(0.8)
                }
                Spacer()
            } else {
                AppGridCollectionView(
@@ -1150,6 +1212,7 @@
                    displayMode: displayMode,
                    iconSize: iconSize,
                    showNames: !hideAppNames,
                    appGridTheme: renderedAppGridTheme,
                    bubbleDisabled: appBubbleDisabled,
                    showUncommonAppBubbles: showUncommonAppBubbles,
                    highlightedGroupName: appGridHighlightedGroupName,
@@ -1241,17 +1304,32 @@
                } label: {
                    Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right")
                        .font(.system(size: 12))
                        .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor)
                        .padding(.horizontal, 8)
                        .frame(height: 28)
                        .background(
                            RoundedRectangle(cornerRadius: 7, style: .continuous)
                                .fill(renderedAppGridTheme.editControlSurfaceColor)
                        )
                        .overlay(
                            RoundedRectangle(cornerRadius: 7, style: .continuous)
                                .stroke(renderedAppGridTheme.editControlStrokeColor, lineWidth: 1)
                        )
                }
                .buttonStyle(.bordered)
                .buttonStyle(.plain)
                .shadow(color: renderedAppGridTheme.editButtonShadowColor, radius: 10, y: 4)
                Spacer()
                Text(tr("edit.tags")).font(.headline)
                Text(tr("edit.tags"))
                    .font(.headline)
                    .foregroundStyle(renderedAppGridTheme.editPrimaryTextColor)
                Spacer()
            }
            .padding(.horizontal, 24)
            .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20)
            .padding(.bottom, 12)
            .background(renderedAppGridTheme.editToolbarSurfaceColor)
            Divider().opacity(0.3)
            Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
            TagEditorView(
                tagColors: $tagColors,
@@ -1266,6 +1344,7 @@
    private var editAppsView: some View {
        VStack(spacing: 0) {
            EditAppsHeaderView(
                theme: renderedAppGridTheme,
                operation: editTagOperation,
                hintText: editModeHintText,
                confirmTitle: editConfirmTitle,
@@ -1279,11 +1358,12 @@
                onConfirm: confirmAssign
            )
            Divider().opacity(0.3)
            Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
            HStack(spacing: 0) {
                VStack(alignment: .leading, spacing: 4) {
                    EditAppsSidebarIntroView(
                        theme: renderedAppGridTheme,
                        width: editSidebarWidth,
                        horizontalInset: editSidebarHorizontalInset
                    )
@@ -1315,10 +1395,14 @@
                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
                }
                .frame(width: editSidebarWidth, alignment: .topLeading)
                Rectangle().fill(.secondary.opacity(0.12)).frame(width: 1)
                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(width: 1)
                if allApps.isEmpty {
                    Spacer(); ProgressView().scaleEffect(0.8); Spacer()
                    Spacer()
                    if loadingSpinnerVisible {
                        ProgressView().scaleEffect(0.8)
                    }
                    Spacer()
                } else {
                    editAppsGrid
                }
@@ -1342,12 +1426,12 @@
    private func editFlatGroup(_ group: TagGroup) -> some View {
        VStack(alignment: .leading, spacing: 0) {
            HStack(spacing: 0) {
                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
                Text(group.name)
                    .font(.system(size: tagFontSize, weight: .semibold))
                    .foregroundStyle(.secondary)
                    .foregroundStyle(renderedAppGridTheme.editSecondaryTextColor)
                    .padding(.horizontal, 10)
                Rectangle().fill(.secondary.opacity(0.25)).frame(height: 1)
                Rectangle().fill(renderedAppGridTheme.editDividerColor).frame(height: 1)
            }
            .padding(.bottom, 6)
@@ -1380,7 +1464,8 @@
            colorIndex: colorIndex,
            operation: editTagOperation,
            isSelected: isSelected,
            isRemovableCandidate: isRemovableCandidate
            isRemovableCandidate: isRemovableCandidate,
            theme: renderedAppGridTheme
        ) {
            if isSelected {
                selectedTagNames.remove(tagName)
@@ -1454,7 +1539,8 @@
        return EditableAppSelectionItem(
            app: app,
            iconSize: iconSize,
            isSelected: isSelected
            isSelected: isSelected,
            theme: renderedAppGridTheme
        ) {
            toggleEditableAppSelection(app)
        }
@@ -1877,16 +1963,24 @@
        // Each overlay show creates a fresh ContentView with empty `allApps`. The indexer
        // cache may still be warm from a prior overlay/settings scan, so path-signature
        // alone must not skip the first hydrate or the grid spinner never clears.
        guard allApps.isEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return }
        let wasEmpty = allApps.isEmpty
        hydrateFromLastAppLibrarySnapshotIfNeeded()
        guard wasEmpty || AppIndexer.shouldRefreshForSearchPathChanges() else { return }
        refreshApps()
    }
    private func refreshAppsForQuickSearch() {
        hydrateFromLastAppLibrarySnapshotIfNeeded()
        guard allApps.isEmpty
            || quickSearchDocuments.isEmpty
            || AppIndexer.shouldRefreshForSearchPathChanges()
        else { return }
        refreshApps()
    }
    private func hydrateFromLastAppLibrarySnapshotIfNeeded() {
        guard allApps.isEmpty, let snapshot = AppLibraryController.lastSnapshot() else { return }
        applyAppLibrarySnapshot(snapshot)
    }
    private func refreshNotchHeight() {
@@ -2534,6 +2628,7 @@
        }
        refreshInProgress = true
        scheduleLoadingSpinnerIfNeeded()
        DispatchQueue.global(qos: .userInitiated).async {
            let result = AppLibraryController.refresh(useCache: useCache)
            DispatchQueue.main.async {
@@ -2543,6 +2638,7 @@
                    finishDropRefreshAfterMinimumDuration()
                }
                refreshInProgress = false
                hideLoadingSpinner()
                if refreshAgainAfterCurrent {
                    let shouldForceLayout = refreshAgainForceLayout
                    let shouldUseCache = refreshAgainUseCache
@@ -2555,6 +2651,25 @@
        }
    }
    private func scheduleLoadingSpinnerIfNeeded() {
        loadingSpinnerToken &+= 1
        let token = loadingSpinnerToken
        loadingSpinnerVisible = false
        guard allApps.isEmpty else { return }
        DispatchQueue.main.asyncAfter(deadline: .now() + loadingSpinnerDelay) {
            guard token == loadingSpinnerToken,
                  refreshInProgress,
                  allApps.isEmpty
            else { return }
            loadingSpinnerVisible = true
        }
    }
    private func hideLoadingSpinner() {
        loadingSpinnerToken &+= 1
        loadingSpinnerVisible = false
    }
    private func applyAppLibrarySnapshot(_ snapshot: AppLibrarySnapshot) {
        allApps = snapshot.apps
        quickSearchDocuments = snapshot.quickSearchDocuments
src/Apptag/EditModeViews.swift
@@ -2,6 +2,7 @@
import AppKit
struct EditAppsHeaderView: View {
    let theme: AppGridTheme
    let operation: EditTagOperation
    let hintText: String
    let confirmTitle: String
@@ -16,19 +17,34 @@
            Button(action: onExit) {
                Label(tr("edit.exit"), systemImage: "rectangle.portrait.and.arrow.right")
                    .font(.system(size: 12))
                    .foregroundStyle(theme.editPrimaryTextColor)
                    .padding(.horizontal, 8)
                    .frame(height: 28)
                    .background(
                        RoundedRectangle(cornerRadius: 7, style: .continuous)
                            .fill(theme.editControlSurfaceColor)
                    )
                    .overlay(
                        RoundedRectangle(cornerRadius: 7, style: .continuous)
                            .stroke(theme.editControlStrokeColor, lineWidth: 1)
                    )
            }
            .buttonStyle(.bordered)
            .buttonStyle(.plain)
            .help(tr("edit.exit"))
            .shadow(color: theme.editButtonShadowColor, radius: 10, y: 4)
            Spacer(minLength: 20)
            VStack(alignment: .trailing, spacing: 5) {
                HStack(alignment: .center, spacing: 14) {
                    EditOperationPicker(
                        theme: theme,
                        operation: operation,
                        onSelect: onSelectOperation
                    )
                    EditConfirmButton(
                        theme: theme,
                        title: confirmTitle,
                        isDisabled: isConfirmDisabled,
                        action: onConfirm
@@ -37,7 +53,7 @@
                Text(hintText)
                    .font(.system(size: 11, weight: .medium))
                    .foregroundStyle(.secondary)
                    .foregroundStyle(theme.editSecondaryTextColor)
                    .lineLimit(2)
                    .multilineTextAlignment(.trailing)
                    .frame(maxWidth: 460, alignment: .trailing)
@@ -48,10 +64,12 @@
        .padding(.horizontal, 24)
        .padding(.top, notchHeight > 0 ? notchHeight + 10 : 20)
        .padding(.bottom, 10)
        .background(theme.editToolbarSurfaceColor)
    }
}
private struct EditOperationPicker: View {
    let theme: AppGridTheme
    let operation: EditTagOperation
    let onSelect: (EditTagOperation) -> Void
@@ -59,16 +77,18 @@
        HStack(spacing: 10) {
            Text(tr("edit.operationLabel"))
                .font(.system(size: 12, weight: .medium))
                .foregroundStyle(.secondary)
                .foregroundStyle(theme.editSecondaryTextColor)
            HStack(spacing: 14) {
                EditOperationModeButton(
                    theme: theme,
                    operation: operation,
                    mode: .add,
                    titleKey: "edit.modeAdd",
                    onSelect: onSelect
                )
                EditOperationModeButton(
                    theme: theme,
                    operation: operation,
                    mode: .remove,
                    titleKey: "edit.modeRemove",
@@ -76,10 +96,22 @@
                )
            }
        }
        .padding(.horizontal, 10)
        .frame(height: 34)
        .background(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .fill(theme.editControlSurfaceColor)
        )
        .overlay(
            RoundedRectangle(cornerRadius: 8, style: .continuous)
                .stroke(theme.editControlStrokeColor, lineWidth: 1)
        )
        .shadow(color: theme.editButtonShadowColor, radius: 10, y: 4)
    }
}
private struct EditOperationModeButton: View {
    let theme: AppGridTheme
    let operation: EditTagOperation
    let mode: EditTagOperation
    let titleKey: String
@@ -97,23 +129,24 @@
                ZStack {
                    Circle()
                        .strokeBorder(
                            isActive ? Color.accentColor : Color.secondary.opacity(0.58),
                            isActive ? theme.editAccentColor : theme.editInactiveIndicatorColor,
                            lineWidth: 1.6
                        )
                        .frame(width: 13, height: 13)
                    if isActive {
                        Circle()
                            .fill(Color.accentColor)
                            .fill(theme.editAccentColor)
                            .frame(width: 7, height: 7)
                    }
                }
                Text(tr(titleKey))
                    .font(.system(size: 12, weight: .semibold))
                    .foregroundStyle(isActive ? Color.primary : Color.secondary)
                    .foregroundStyle(isActive ? theme.editPrimaryTextColor : theme.editSecondaryTextColor)
                    .lineLimit(1)
            }
            .padding(.vertical, 4)
            .padding(.horizontal, 2)
            .contentShape(Rectangle())
        }
        .buttonStyle(.plain)
@@ -123,6 +156,7 @@
}
private struct EditConfirmButton: View {
    let theme: AppGridTheme
    let title: String
    let isDisabled: Bool
    let action: () -> Void
@@ -131,7 +165,7 @@
        Button(action: action) {
            Text(title)
                .font(.system(size: 13, weight: .semibold))
                .foregroundStyle(Color.white.opacity(isDisabled ? 0.76 : 1.0))
                .foregroundStyle(isDisabled ? theme.editDisabledTextColor : theme.editConfirmForegroundColor)
                .lineLimit(1)
                .minimumScaleFactor(0.82)
                .frame(minWidth: 116)
@@ -139,20 +173,22 @@
                .padding(.horizontal, 8)
                .background(
                    RoundedRectangle(cornerRadius: 8, style: .continuous)
                        .fill(Color.accentColor.opacity(isDisabled ? 0.40 : 1.0))
                        .fill(isDisabled ? theme.editDisabledSurfaceColor : theme.editAccentColor)
                )
                .overlay(
                    RoundedRectangle(cornerRadius: 8, style: .continuous)
                        .stroke(Color.accentColor.opacity(isDisabled ? 0.18 : 0.24), lineWidth: 1)
                        .stroke(isDisabled ? theme.editControlStrokeColor : theme.editAccentColor.opacity(0.72), lineWidth: 1)
                )
        }
        .buttonStyle(.plain)
        .disabled(isDisabled)
        .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
        .shadow(color: isDisabled ? Color.clear : theme.editButtonShadowColor, radius: 12, y: 5)
    }
}
struct EditAppsSidebarIntroView: View {
    let theme: AppGridTheme
    let width: CGFloat
    let horizontalInset: CGFloat
@@ -160,7 +196,7 @@
        VStack(alignment: .leading, spacing: 6) {
            Text(tr("edit.selectTags"))
                .font(.caption)
                .foregroundStyle(.secondary)
                .foregroundStyle(theme.editSecondaryTextColor)
                .lineLimit(2)
                .fixedSize(horizontal: false, vertical: true)
                .multilineTextAlignment(.leading)
@@ -168,7 +204,7 @@
            Text(tr("edit.dragHint"))
                .font(.caption2)
                .foregroundStyle(.tertiary)
                .foregroundStyle(theme.editTertiaryTextColor)
                .lineLimit(nil)
                .fixedSize(horizontal: false, vertical: true)
                .multilineTextAlignment(.leading)
@@ -188,6 +224,7 @@
    let operation: EditTagOperation
    let isSelected: Bool
    let isRemovableCandidate: Bool
    let theme: AppGridTheme
    let onToggle: () -> Void
    private var isEnabled: Bool {
@@ -205,7 +242,7 @@
    var body: some View {
        HStack(spacing: 6) {
            Circle()
                .fill(isSelected ? Color.accentColor : Color.secondary.opacity(isEnabled ? 0.3 : 0.18))
                .fill(isSelected ? theme.editAccentColor : theme.editInactiveIndicatorColor.opacity(isEnabled ? 1.0 : 0.56))
                .frame(width: 16, height: 16)
                .overlay(
                    isSelected
@@ -217,7 +254,7 @@
            Text(displayName)
                .font(.system(size: 13, weight: isUncommon ? .semibold : .medium))
                .foregroundStyle(isEnabled ? Color.primary : Color.secondary.opacity(0.66))
                .foregroundStyle(isEnabled ? theme.editPrimaryTextColor : theme.editDisabledTextColor)
                .lineLimit(1)
                .truncationMode(.tail)
                .frame(maxWidth: .infinity, alignment: .leading)
@@ -226,13 +263,13 @@
                if isUncommon {
                    Image(systemName: "questionmark.bubble.fill")
                        .font(.system(size: 11, weight: .semibold))
                        .foregroundStyle(isEnabled ? Color.secondary : Color.secondary.opacity(0.55))
                        .foregroundStyle(isEnabled ? theme.editSecondaryTextColor : theme.editDisabledTextColor)
                        .frame(width: 16, height: 16)
                }
                if showsPendingRemoval {
                    Image(systemName: "trash.fill")
                        .font(.system(size: 11, weight: .bold))
                        .foregroundStyle(.red)
                        .foregroundStyle(Color.red.opacity(theme.usesDarkGlass ? 0.96 : 0.84))
                        .frame(width: 16, height: 16)
                        .transition(.scale(scale: 0.85).combined(with: .opacity))
                } else if operation == .remove {
@@ -255,9 +292,13 @@
                .fill(
                    Color(
                        nsColor: TagColor.nsColor(for: colorIndex)
                            .withAlphaComponent(isEnabled ? (isUncommon ? 0.22 : 0.3) : 0.12)
                            .withAlphaComponent(isEnabled ? (isUncommon ? 0.26 : 0.34) : 0.16)
                    )
                )
        )
        .overlay(
            RoundedRectangle(cornerRadius: 6)
                .stroke(theme.editControlStrokeColor.opacity(isSelected ? 1.0 : 0.44), lineWidth: isSelected ? 1.1 : 0.6)
        )
        .contentShape(RoundedRectangle(cornerRadius: 6))
        .onTapGesture {
@@ -273,6 +314,7 @@
    let app: AppInfo
    let iconSize: CGFloat
    let isSelected: Bool
    let theme: AppGridTheme
    let onToggle: () -> Void
    var body: some View {
@@ -285,7 +327,7 @@
                        .frame(width: iconSize, height: iconSize)
                    Circle()
                        .fill(isSelected ? Color.accentColor : Color.secondary.opacity(0.3))
                        .fill(isSelected ? theme.editAccentColor : theme.editInactiveIndicatorColor)
                        .frame(width: 20, height: 20)
                        .overlay(
                            isSelected
@@ -299,6 +341,7 @@
                Text(app.displayName)
                    .font(.system(size: 11, weight: .medium))
                    .foregroundStyle(theme.editPrimaryTextColor)
                    .lineLimit(1)
                    .truncationMode(.tail)
                    .frame(maxWidth: iconSize + 20)
@@ -307,6 +350,14 @@
            .padding(.horizontal, 4)
            .contentShape(RoundedRectangle(cornerRadius: 10))
            .opacity(isSelected ? 1.0 : 0.65)
            .background(
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .fill(isSelected ? theme.editControlSurfaceColor : Color.clear)
            )
            .overlay(
                RoundedRectangle(cornerRadius: 10, style: .continuous)
                    .stroke(isSelected ? theme.editControlStrokeColor : Color.clear, lineWidth: 1)
            )
        }
        .buttonStyle(.plain)
    }
src/Apptag/Info.plist
@@ -19,9 +19,9 @@
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleShortVersionString</key>
    <string>7.9.4</string>
    <string>8.0.1</string>
    <key>CFBundleVersion</key>
    <string>20260619.2221</string>
    <string>20260624.2021</string>
    <key>LSApplicationCategoryType</key>
    <string>public.app-category.utilities</string>
    <key>LSMinimumSystemVersion</key>
src/Apptag/Localization/ar-Najdi.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "لغة التطبيق:",
  "settings.languageDesc": "اختر لغة القوائم والإعدادات.",
  "settings.general": "عام",
  "settings.theme": "السمة",
  "settings.themeDesc": "اختر سمة خلفية شبكة التطبيقات.",
  "settings.themeScopeDesc": "السمات تغيّر خلفية شبكة التطبيقات فقط. الحاويات تبقى بسطح زجاجي موحّد.",
  "settings.tags": "الوسوم",
  "settings.data": "البيانات",
  "settings.about": "عن التطبيق",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "تلميحة 7 - ترتيب التطبيقات",
  "usageTips.tip7.detail": "اضغط مطوّل على التطبيق, اسحبه لمكان داخل الحاوية.",
  "usageTips.tip8.title": "تلميحة 8 - تعديل الملاحظات",
  "usageTips.tip8.detail": "انقر يمين على التطبيق, اكتب ملاحظة."
  "usageTips.tip8.detail": "انقر يمين على التطبيق, اكتب ملاحظة.",
  "theme.default": "الافتراضية",
  "theme.deepBlue": "أزرق عميق",
  "theme.black": "أسود",
  "theme.pink": "وردي",
  "theme.purple": "بنفسجي",
  "theme.green": "أخضر",
  "theme.blue": "أزرق",
  "theme.colorful": "ملوّن"
}
src/Apptag/Localization/ar.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "لغة التطبيق:",
  "settings.languageDesc": "اختر اللغة المستخدمة في القوائم والإعدادات.",
  "settings.general": "عام",
  "settings.theme": "السمة",
  "settings.themeDesc": "اختر سمة خلفية شبكة التطبيقات.",
  "settings.themeScopeDesc": "تغيّر السمات خلفية شبكة التطبيقات فقط. تبقى الحاويات بسطح زجاجي موحّد.",
  "settings.tags": "الوسوم",
  "settings.data": "البيانات",
  "settings.about": "حول",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "النصيحة 7 - ترتيب التطبيقات",
  "usageTips.tip7.detail": "اضغط مطولاً على تطبيق, اسحبه إلى موضع داخل الحاوية.",
  "usageTips.tip8.title": "النصيحة 8 - تحرير الملاحظات",
  "usageTips.tip8.detail": "انقر بزر الماوس الأيمن على تطبيق, أدخل ملاحظة."
  "usageTips.tip8.detail": "انقر بزر الماوس الأيمن على تطبيق, أدخل ملاحظة.",
  "theme.default": "الافتراضية",
  "theme.deepBlue": "أزرق عميق",
  "theme.black": "أسود",
  "theme.pink": "وردي",
  "theme.purple": "بنفسجي",
  "theme.green": "أخضر",
  "theme.blue": "أزرق",
  "theme.colorful": "ملوّن"
}
src/Apptag/Localization/cs.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Jazyk aplikace:",
  "settings.languageDesc": "Vyberte jazyk používaný v nabídkách a nastaveních.",
  "settings.general": "Obecné",
  "settings.theme": "Motiv",
  "settings.themeDesc": "Vyberte motiv pozadí mřížky aplikací.",
  "settings.themeScopeDesc": "Motivy mění jen pozadí mřížky aplikací. Kontejnery si ponechají jednotný skleněný povrch.",
  "settings.tags": "Štítky",
  "settings.data": "Data",
  "settings.about": "O aplikaci",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tip 7 - Seřadit aplikace",
  "usageTips.tip7.detail": "Podržte aplikaci, přetáhněte ji na místo v kontejneru.",
  "usageTips.tip8.title": "Tip 8 - Upravit poznámky",
  "usageTips.tip8.detail": "Klikněte na aplikaci pravým tlačítkem, zadejte poznámku."
  "usageTips.tip8.detail": "Klikněte na aplikaci pravým tlačítkem, zadejte poznámku.",
  "theme.default": "Výchozí",
  "theme.deepBlue": "Tmavě modrý",
  "theme.black": "Černý",
  "theme.pink": "Růžový",
  "theme.purple": "Fialový",
  "theme.green": "Zelený",
  "theme.blue": "Modrý",
  "theme.colorful": "Barevný"
}
src/Apptag/Localization/da.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Appsprog:",
  "settings.languageDesc": "Vælg sproget til menuer og indstillinger.",
  "settings.general": "Generelt",
  "settings.theme": "Tema",
  "settings.themeDesc": "Vælg baggrundstema for appgitteret.",
  "settings.themeScopeDesc": "Temaer ændrer kun baggrunden i appgitteret. Containere beholder en ensartet glasflade.",
  "settings.tags": "Tags",
  "settings.data": "Data",
  "settings.about": "Om",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tip 7 - Sortér apps",
  "usageTips.tip7.detail": "Hold en app nede, flyt den i containeren.",
  "usageTips.tip8.title": "Tip 8 - Rediger noter",
  "usageTips.tip8.detail": "Højreklik på en app, indtast en note."
  "usageTips.tip8.detail": "Højreklik på en app, indtast en note.",
  "theme.default": "Standard",
  "theme.deepBlue": "Dyb blå",
  "theme.black": "Sort",
  "theme.pink": "Pink",
  "theme.purple": "Lilla",
  "theme.green": "Grøn",
  "theme.blue": "Blå",
  "theme.colorful": "Farverig"
}
src/Apptag/Localization/de.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "App-Sprache:",
  "settings.languageDesc": "Wähle die Sprache für Menüs und Einstellungen.",
  "settings.general": "Allgemein",
  "settings.theme": "Design",
  "settings.themeDesc": "Wähle das Hintergrunddesign für das App-Raster.",
  "settings.themeScopeDesc": "Designs ändern nur den Hintergrund des App-Rasters. Container behalten eine einheitliche Glasfläche.",
  "settings.tags": "Tags",
  "settings.data": "Daten",
  "settings.about": "Über",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tipp 7 - Apps sortieren",
  "usageTips.tip7.detail": "App gedrückt halten, zur Position im Container ziehen.",
  "usageTips.tip8.title": "Tipp 8 - Notizen bearbeiten",
  "usageTips.tip8.detail": "Rechtsklick auf App, Notiz eingeben."
  "usageTips.tip8.detail": "Rechtsklick auf App, Notiz eingeben.",
  "theme.default": "Standard",
  "theme.deepBlue": "Tiefblau",
  "theme.black": "Schwarz",
  "theme.pink": "Pink",
  "theme.purple": "Violett",
  "theme.green": "Grün",
  "theme.blue": "Blau",
  "theme.colorful": "Bunt"
}
src/Apptag/Localization/en.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "App language:",
  "settings.languageDesc": "Choose the language used by menus and settings.",
  "settings.general": "General",
  "settings.theme": "Theme",
  "settings.themeDesc": "Choose the App Grid background theme.",
  "settings.themeScopeDesc": "Themes only change the App Grid background. Containers keep one consistent glass surface.",
  "settings.tags": "Tags",
  "settings.data": "Data",
  "settings.about": "About",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tip 7 - Reorder apps",
  "usageTips.tip7.detail": "Long-press an app, then drag it to a spot in the container.",
  "usageTips.tip8.title": "Tip 8 - Edit notes",
  "usageTips.tip8.detail": "Right-click an app, enter a note."
  "usageTips.tip8.detail": "Right-click an app, enter a note.",
  "theme.default": "Default",
  "theme.deepBlue": "Deep Blue",
  "theme.black": "Black",
  "theme.pink": "Pink",
  "theme.purple": "Purple",
  "theme.green": "Green",
  "theme.blue": "Blue",
  "theme.colorful": "Colorful"
}
src/Apptag/Localization/es.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Idioma de la app:",
  "settings.languageDesc": "Elige el idioma usado en los menús y ajustes.",
  "settings.general": "General",
  "settings.theme": "Tema",
  "settings.themeDesc": "Elige el tema de fondo de la cuadrícula de apps.",
  "settings.themeScopeDesc": "Los temas solo cambian el fondo de la cuadrícula de apps. Los contenedores mantienen una superficie de cristal uniforme.",
  "settings.tags": "Etiquetas",
  "settings.data": "Datos",
  "settings.about": "Acerca de",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Consejo 7 - Ordenar apps",
  "usageTips.tip7.detail": "Mantén pulsada una app, arrástrala a una posición del contenedor.",
  "usageTips.tip8.title": "Consejo 8 - Editar notas",
  "usageTips.tip8.detail": "Clic derecho en una app, escriba una nota."
  "usageTips.tip8.detail": "Clic derecho en una app, escriba una nota.",
  "theme.default": "Predeterminado",
  "theme.deepBlue": "Azul profundo",
  "theme.black": "Negro",
  "theme.pink": "Rosa",
  "theme.purple": "Morado",
  "theme.green": "Verde",
  "theme.blue": "Azul",
  "theme.colorful": "Colorido"
}
src/Apptag/Localization/fr.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Langue de l'app :",
  "settings.languageDesc": "Choisissez la langue utilisée par les menus et les réglages.",
  "settings.general": "Général",
  "settings.theme": "Thème",
  "settings.themeDesc": "Choisissez le thème d’arrière-plan de la grille d’apps.",
  "settings.themeScopeDesc": "Les thèmes ne modifient que l’arrière-plan de la grille d’apps. Les conteneurs gardent une surface vitrée uniforme.",
  "settings.tags": "Étiquettes",
  "settings.data": "Données",
  "settings.about": "À propos",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Astuce 7 - Trier les apps",
  "usageTips.tip7.detail": "Appuyez longuement sur une app, déposez-la dans le conteneur.",
  "usageTips.tip8.title": "Astuce 8 - Modifier les notes",
  "usageTips.tip8.detail": "Clic droit sur une app, saisissez une note."
  "usageTips.tip8.detail": "Clic droit sur une app, saisissez une note.",
  "theme.default": "Par défaut",
  "theme.deepBlue": "Bleu profond",
  "theme.black": "Noir",
  "theme.pink": "Rose",
  "theme.purple": "Violet",
  "theme.green": "Vert",
  "theme.blue": "Bleu",
  "theme.colorful": "Coloré"
}
src/Apptag/Localization/id.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Bahasa aplikasi:",
  "settings.languageDesc": "Pilih bahasa yang digunakan untuk menu dan pengaturan.",
  "settings.general": "Umum",
  "settings.theme": "Tema",
  "settings.themeDesc": "Pilih tema latar belakang grid app.",
  "settings.themeScopeDesc": "Tema hanya mengubah latar belakang grid app. Kontainer tetap memakai permukaan kaca yang konsisten.",
  "settings.tags": "Tag",
  "settings.data": "Data",
  "settings.about": "Tentang",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tips 7 - Urutkan app",
  "usageTips.tip7.detail": "Tekan lama app, pindahkan di dalam kontainer.",
  "usageTips.tip8.title": "Tips 8 - Edit catatan",
  "usageTips.tip8.detail": "Klik kanan app, masukkan catatan."
  "usageTips.tip8.detail": "Klik kanan app, masukkan catatan.",
  "theme.default": "Default",
  "theme.deepBlue": "Biru Tua",
  "theme.black": "Hitam",
  "theme.pink": "Merah Muda",
  "theme.purple": "Ungu",
  "theme.green": "Hijau",
  "theme.blue": "Biru",
  "theme.colorful": "Berwarna"
}
src/Apptag/Localization/it.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Lingua app:",
  "settings.languageDesc": "Scegli la lingua usata da menu e impostazioni.",
  "settings.general": "Generali",
  "settings.theme": "Tema",
  "settings.themeDesc": "Scegli il tema di sfondo della griglia delle app.",
  "settings.themeScopeDesc": "I temi cambiano solo lo sfondo della griglia delle app. I contenitori mantengono una superficie in vetro uniforme.",
  "settings.tags": "Tag",
  "settings.data": "Dati",
  "settings.about": "Informazioni",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Suggerimento 7 - Riordinare app",
  "usageTips.tip7.detail": "Tieni premuta un’app, spostala nel contenitore.",
  "usageTips.tip8.title": "Suggerimento 8 - Modificare note",
  "usageTips.tip8.detail": "Fai clic destro su un’app, inserisci una nota."
  "usageTips.tip8.detail": "Fai clic destro su un’app, inserisci una nota.",
  "theme.default": "Predefinito",
  "theme.deepBlue": "Blu profondo",
  "theme.black": "Nero",
  "theme.pink": "Rosa",
  "theme.purple": "Viola",
  "theme.green": "Verde",
  "theme.blue": "Blu",
  "theme.colorful": "Colorato"
}
src/Apptag/Localization/ja.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "アプリの言語:",
  "settings.languageDesc": "メニューと設定で使う言語を選択します。",
  "settings.general": "一般",
  "settings.theme": "テーマ",
  "settings.themeDesc": "アプリグリッドの背景テーマを選びます。",
  "settings.themeScopeDesc": "テーマはアプリグリッドの背景だけを変更します。コンテナは統一されたガラス面のままです。",
  "settings.tags": "タグ",
  "settings.data": "データ",
  "settings.about": "概要",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "ヒント7 - アプリを並べ替え",
  "usageTips.tip7.detail": "アプリを長押し, コンテナ内の目的位置へ移動します。",
  "usageTips.tip8.title": "ヒント8 - メモを編集",
  "usageTips.tip8.detail": "アプリを右クリック, メモを入力します。"
  "usageTips.tip8.detail": "アプリを右クリック, メモを入力します。",
  "theme.default": "デフォルト",
  "theme.deepBlue": "ディープブルー",
  "theme.black": "ブラック",
  "theme.pink": "ピンク",
  "theme.purple": "パープル",
  "theme.green": "グリーン",
  "theme.blue": "ブルー",
  "theme.colorful": "カラフル"
}
src/Apptag/Localization/ko.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "앱 언어:",
  "settings.languageDesc": "메뉴와 설정에서 사용할 언어를 선택합니다.",
  "settings.general": "일반",
  "settings.theme": "테마",
  "settings.themeDesc": "앱 그리드 배경 테마를 선택합니다.",
  "settings.themeScopeDesc": "테마는 앱 그리드 배경만 바꿉니다. 컨테이너는 일관된 유리 표면을 유지합니다.",
  "settings.tags": "태그",
  "settings.data": "데이터",
  "settings.about": "정보",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "팁 7 - 앱 정렬",
  "usageTips.tip7.detail": "앱을 길게 누르기, 컨테이너 안에서 이동.",
  "usageTips.tip8.title": "팁 8 - 메모 편집",
  "usageTips.tip8.detail": "앱을 오른쪽 클릭, 메모 입력."
  "usageTips.tip8.detail": "앱을 오른쪽 클릭, 메모 입력.",
  "theme.default": "기본",
  "theme.deepBlue": "딥 블루",
  "theme.black": "검정",
  "theme.pink": "분홍",
  "theme.purple": "보라",
  "theme.green": "초록",
  "theme.blue": "파랑",
  "theme.colorful": "컬러풀"
}
src/Apptag/Localization/ms.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Bahasa aplikasi:",
  "settings.languageDesc": "Pilih bahasa yang digunakan untuk menu dan tetapan.",
  "settings.general": "Umum",
  "settings.theme": "Tema",
  "settings.themeDesc": "Pilih tema latar belakang grid app.",
  "settings.themeScopeDesc": "Tema hanya menukar latar belakang grid app. Kontainer kekal dengan permukaan kaca yang konsisten.",
  "settings.tags": "Tag",
  "settings.data": "Data",
  "settings.about": "Tentang",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Petua 7 - Susun app",
  "usageTips.tip7.detail": "Tekan lama app, alih dalam bekas.",
  "usageTips.tip8.title": "Petua 8 - Edit nota",
  "usageTips.tip8.detail": "Klik kanan app, masukkan nota."
  "usageTips.tip8.detail": "Klik kanan app, masukkan nota.",
  "theme.default": "Lalai",
  "theme.deepBlue": "Biru Gelap",
  "theme.black": "Hitam",
  "theme.pink": "Merah Jambu",
  "theme.purple": "Ungu",
  "theme.green": "Hijau",
  "theme.blue": "Biru",
  "theme.colorful": "Berwarna-warni"
}
src/Apptag/Localization/nb.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Appspråk:",
  "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.",
  "settings.general": "Generelt",
  "settings.theme": "Tema",
  "settings.themeDesc": "Velg bakgrunnstema for app-rutenettet.",
  "settings.themeScopeDesc": "Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.",
  "settings.tags": "Etiketter",
  "settings.data": "Data",
  "settings.about": "Om",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tips 7 - Sorter apper",
  "usageTips.tip7.detail": "Hold en app inne, flytt den i beholderen.",
  "usageTips.tip8.title": "Tips 8 - Rediger notater",
  "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat."
  "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat.",
  "theme.default": "Standard",
  "theme.deepBlue": "Dyp blå",
  "theme.black": "Svart",
  "theme.pink": "Rosa",
  "theme.purple": "Lilla",
  "theme.green": "Grønn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
}
src/Apptag/Localization/nl.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "App-taal:",
  "settings.languageDesc": "Kies de taal voor menu’s en instellingen.",
  "settings.general": "Algemeen",
  "settings.theme": "Thema",
  "settings.themeDesc": "Kies het achtergrondthema voor het app-raster.",
  "settings.themeScopeDesc": "Thema’s wijzigen alleen de achtergrond van het app-raster. Containers behouden één consistente glaslaag.",
  "settings.tags": "Tags",
  "settings.data": "Gegevens",
  "settings.about": "Over",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tip 7 - Apps sorteren",
  "usageTips.tip7.detail": "Houd een app ingedrukt, sleep binnen de container.",
  "usageTips.tip8.title": "Tip 8 - Notities bewerken",
  "usageTips.tip8.detail": "Klik met rechts op een app, voer een notitie in."
  "usageTips.tip8.detail": "Klik met rechts op een app, voer een notitie in.",
  "theme.default": "Standaard",
  "theme.deepBlue": "Diepblauw",
  "theme.black": "Zwart",
  "theme.pink": "Roze",
  "theme.purple": "Paars",
  "theme.green": "Groen",
  "theme.blue": "Blauw",
  "theme.colorful": "Kleurrijk"
}
src/Apptag/Localization/nn.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Appspråk:",
  "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.",
  "settings.general": "Generelt",
  "settings.theme": "Tema",
  "settings.themeDesc": "Vel bakgrunnstema for apprutenettet.",
  "settings.themeScopeDesc": "Tema endrar berre bakgrunnen i apprutenettet. Behaldarane har éi jamn glasflate.",
  "settings.tags": "Etiketter",
  "settings.data": "Data",
  "settings.about": "Om",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tips 7 - Sorter apper",
  "usageTips.tip7.detail": "Hold ein app inne, flytt han i behaldaren.",
  "usageTips.tip8.title": "Tips 8 - Rediger notater",
  "usageTips.tip8.detail": "Høgreklikk på ein app, skriv inn eit notat."
  "usageTips.tip8.detail": "Høgreklikk på ein app, skriv inn eit notat.",
  "theme.default": "Standard",
  "theme.deepBlue": "Djup blå",
  "theme.black": "Svart",
  "theme.pink": "Rosa",
  "theme.purple": "Lilla",
  "theme.green": "Grøn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
}
src/Apptag/Localization/no.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Appspråk:",
  "settings.languageDesc": "Velg språket som brukes i menyer og innstillinger.",
  "settings.general": "Generelt",
  "settings.theme": "Tema",
  "settings.themeDesc": "Velg bakgrunnstema for app-rutenettet.",
  "settings.themeScopeDesc": "Temaer endrer bare bakgrunnen i app-rutenettet. Beholdere har én konsekvent glassflate.",
  "settings.tags": "Etiketter",
  "settings.data": "Data",
  "settings.about": "Om",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tips 7 - Sorter apper",
  "usageTips.tip7.detail": "Hold en app inne, flytt den i beholderen.",
  "usageTips.tip8.title": "Tips 8 - Rediger notater",
  "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat."
  "usageTips.tip8.detail": "Høyreklikk på en app, skriv inn et notat.",
  "theme.default": "Standard",
  "theme.deepBlue": "Dyp blå",
  "theme.black": "Svart",
  "theme.pink": "Rosa",
  "theme.purple": "Lilla",
  "theme.green": "Grønn",
  "theme.blue": "Blå",
  "theme.colorful": "Fargerik"
}
src/Apptag/Localization/pl.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Język aplikacji:",
  "settings.languageDesc": "Wybierz język menu i ustawień.",
  "settings.general": "Ogólne",
  "settings.theme": "Motyw",
  "settings.themeDesc": "Wybierz motyw tła siatki aplikacji.",
  "settings.themeScopeDesc": "Motywy zmieniają tylko tło siatki aplikacji. Kontenery zachowują jednolitą szklaną powierzchnię.",
  "settings.tags": "Tagi",
  "settings.data": "Dane",
  "settings.about": "O aplikacji",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Wskazówka 7 - Sortuj aplikacje",
  "usageTips.tip7.detail": "Przytrzymaj aplikację, przeciągnij ją w kontenerze.",
  "usageTips.tip8.title": "Wskazówka 8 - Edytuj notatki",
  "usageTips.tip8.detail": "Kliknij aplikację prawym przyciskiem, wpisz notatkę."
  "usageTips.tip8.detail": "Kliknij aplikację prawym przyciskiem, wpisz notatkę.",
  "theme.default": "Domyślny",
  "theme.deepBlue": "Głęboki niebieski",
  "theme.black": "Czarny",
  "theme.pink": "Różowy",
  "theme.purple": "Fioletowy",
  "theme.green": "Zielony",
  "theme.blue": "Niebieski",
  "theme.colorful": "Kolorowy"
}
src/Apptag/Localization/pt-BR.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Idioma do app:",
  "settings.languageDesc": "Escolha o idioma usado nos menus e ajustes.",
  "settings.general": "Geral",
  "settings.theme": "Tema",
  "settings.themeDesc": "Escolha o tema de fundo da grade de apps.",
  "settings.themeScopeDesc": "Os temas mudam apenas o fundo da grade de apps. Os contêineres mantêm uma superfície de vidro consistente.",
  "settings.tags": "Etiquetas",
  "settings.data": "Dados",
  "settings.about": "Sobre",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Dica 7 - Reordenar apps",
  "usageTips.tip7.detail": "Pressione e segure um app, mova dentro do contêiner.",
  "usageTips.tip8.title": "Dica 8 - Editar notas",
  "usageTips.tip8.detail": "Clique com o botão direito em um app, digite uma nota."
  "usageTips.tip8.detail": "Clique com o botão direito em um app, digite uma nota.",
  "theme.default": "Padrão",
  "theme.deepBlue": "Azul profundo",
  "theme.black": "Preto",
  "theme.pink": "Rosa",
  "theme.purple": "Roxo",
  "theme.green": "Verde",
  "theme.blue": "Azul",
  "theme.colorful": "Colorido"
}
src/Apptag/Localization/ro.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Limba aplicației:",
  "settings.languageDesc": "Alege limba folosită pentru meniuri și setări.",
  "settings.general": "General",
  "settings.theme": "Temă",
  "settings.themeDesc": "Alege tema fundalului pentru grila de aplicații.",
  "settings.themeScopeDesc": "Temele schimbă doar fundalul grilei de aplicații. Containerele păstrează o suprafață de sticlă uniformă.",
  "settings.tags": "Etichete",
  "settings.data": "Date",
  "settings.about": "Despre",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Sfat 7 - Reordonează aplicații",
  "usageTips.tip7.detail": "Ține apăsată o aplicație, mut-o în container.",
  "usageTips.tip8.title": "Sfat 8 - Editează notițe",
  "usageTips.tip8.detail": "Click dreapta pe o aplicație, introdu o notiță."
  "usageTips.tip8.detail": "Click dreapta pe o aplicație, introdu o notiță.",
  "theme.default": "Implicită",
  "theme.deepBlue": "Albastru profund",
  "theme.black": "Negru",
  "theme.pink": "Roz",
  "theme.purple": "Mov",
  "theme.green": "Verde",
  "theme.blue": "Albastru",
  "theme.colorful": "Colorată"
}
src/Apptag/Localization/ru.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Язык приложения:",
  "settings.languageDesc": "Выберите язык меню и настроек.",
  "settings.general": "Общие",
  "settings.theme": "Тема",
  "settings.themeDesc": "Выберите тему фона сетки приложений.",
  "settings.themeScopeDesc": "Темы меняют только фон сетки приложений. Контейнеры сохраняют единый стеклянный вид.",
  "settings.tags": "Теги",
  "settings.data": "Данные",
  "settings.about": "О программе",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Совет 7 — Упорядочить приложения",
  "usageTips.tip7.detail": "Удерживайте приложение, перетащите внутри контейнера.",
  "usageTips.tip8.title": "Совет 8 — Редактировать заметки",
  "usageTips.tip8.detail": "Щелкните приложение правой кнопкой, введите заметку."
  "usageTips.tip8.detail": "Щелкните приложение правой кнопкой, введите заметку.",
  "theme.default": "По умолчанию",
  "theme.deepBlue": "Глубокий синий",
  "theme.black": "Чёрный",
  "theme.pink": "Розовый",
  "theme.purple": "Фиолетовый",
  "theme.green": "Зелёный",
  "theme.blue": "Синий",
  "theme.colorful": "Цветной"
}
src/Apptag/Localization/sr-Cyrl.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Језик апликације:",
  "settings.languageDesc": "Изаберите језик менија и подешавања.",
  "settings.general": "Опште",
  "settings.theme": "Тема",
  "settings.themeDesc": "Изаберите тему позадине мреже апликација.",
  "settings.themeScopeDesc": "Теме мењају само позадину мреже апликација. Контејнери задржавају једноличну стаклену површину.",
  "settings.tags": "Ознаке",
  "settings.data": "Data",
  "settings.about": "О програму",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Савет 7 - Поређај апликације",
  "usageTips.tip7.detail": "Држите апликацију, превуците је унутар контејнера.",
  "usageTips.tip8.title": "Савет 8 - Уреди белешке",
  "usageTips.tip8.detail": "Кликните десним тастером на апликацију, унесите белешку."
  "usageTips.tip8.detail": "Кликните десним тастером на апликацију, унесите белешку.",
  "theme.default": "Подразумевана",
  "theme.deepBlue": "Дубока плава",
  "theme.black": "Црна",
  "theme.pink": "Розе",
  "theme.purple": "Љубичаста",
  "theme.green": "Зелена",
  "theme.blue": "Плава",
  "theme.colorful": "Шарена"
}
src/Apptag/Localization/sv.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Appspråk:",
  "settings.languageDesc": "Välj språket som används i menyer och inställningar.",
  "settings.general": "Allmänt",
  "settings.theme": "Tema",
  "settings.themeDesc": "Välj bakgrundstema för apprutnätet.",
  "settings.themeScopeDesc": "Teman ändrar bara bakgrunden i apprutnätet. Behållare har en enhetlig glasyta.",
  "settings.tags": "Taggar",
  "settings.data": "Data",
  "settings.about": "Om",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Tips 7 - Sortera appar",
  "usageTips.tip7.detail": "Håll ned en app, flytta den i containern.",
  "usageTips.tip8.title": "Tips 8 - Redigera anteckningar",
  "usageTips.tip8.detail": "Högerklicka på en app, skriv en anteckning."
  "usageTips.tip8.detail": "Högerklicka på en app, skriv en anteckning.",
  "theme.default": "Standard",
  "theme.deepBlue": "Djupblå",
  "theme.black": "Svart",
  "theme.pink": "Rosa",
  "theme.purple": "Lila",
  "theme.green": "Grön",
  "theme.blue": "Blå",
  "theme.colorful": "Färgstark"
}
src/Apptag/Localization/th.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "ภาษาของแอป:",
  "settings.languageDesc": "เลือกภาษาที่ใช้ในเมนูและการตั้งค่า",
  "settings.general": "ทั่วไป",
  "settings.theme": "ธีม",
  "settings.themeDesc": "เลือกธีมพื้นหลังของกริดแอป",
  "settings.themeScopeDesc": "ธีมจะเปลี่ยนเฉพาะพื้นหลังของกริดแอป คอนเทนเนอร์ยังคงเป็นพื้นผิวกระจกแบบเดียวกัน",
  "settings.tags": "แท็ก",
  "settings.data": "ข้อมูล",
  "settings.about": "เกี่ยวกับ",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "เคล็ดลับ 7 - จัดลำดับแอป",
  "usageTips.tip7.detail": "กดแอปค้างไว้, ลากภายในคอนเทนเนอร์",
  "usageTips.tip8.title": "เคล็ดลับ 8 - แก้ไขโน้ต",
  "usageTips.tip8.detail": "คลิกขวาที่แอป, ป้อนโน้ต"
  "usageTips.tip8.detail": "คลิกขวาที่แอป, ป้อนโน้ต",
  "theme.default": "ค่าเริ่มต้น",
  "theme.deepBlue": "น้ำเงินเข้ม",
  "theme.black": "ดำ",
  "theme.pink": "ชมพู",
  "theme.purple": "ม่วง",
  "theme.green": "เขียว",
  "theme.blue": "น้ำเงิน",
  "theme.colorful": "หลากสี"
}
src/Apptag/Localization/tr.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Uygulama dili:",
  "settings.languageDesc": "Menülerde ve ayarlarda kullanılan dili seçin.",
  "settings.general": "Genel",
  "settings.theme": "Tema",
  "settings.themeDesc": "Uygulama ızgarası arka plan temasını seçin.",
  "settings.themeScopeDesc": "Temalar yalnızca uygulama ızgarası arka planını değiştirir. Kapsayıcılar tutarlı bir cam yüzey olarak kalır.",
  "settings.tags": "Etiketler",
  "settings.data": "Veri",
  "settings.about": "Hakkında",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "İpucu 7 - Uygulamaları sırala",
  "usageTips.tip7.detail": "Bir uygulamaya uzun basın, kapsayıcı içinde taşıyın.",
  "usageTips.tip8.title": "İpucu 8 - Notları düzenle",
  "usageTips.tip8.detail": "Bir uygulamaya sağ tıklayın, not girin."
  "usageTips.tip8.detail": "Bir uygulamaya sağ tıklayın, not girin.",
  "theme.default": "Varsayılan",
  "theme.deepBlue": "Derin Mavi",
  "theme.black": "Siyah",
  "theme.pink": "Pembe",
  "theme.purple": "Mor",
  "theme.green": "Yeşil",
  "theme.blue": "Mavi",
  "theme.colorful": "Renkli"
}
src/Apptag/Localization/uk.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Мова застосунку:",
  "settings.languageDesc": "Оберіть мову меню та налаштувань.",
  "settings.general": "Загальні",
  "settings.theme": "Тема",
  "settings.themeDesc": "Виберіть тему тла сітки програм.",
  "settings.themeScopeDesc": "Теми змінюють лише тло сітки програм. Контейнери зберігають єдину скляну поверхню.",
  "settings.tags": "Теги",
  "settings.data": "Дані",
  "settings.about": "Про програму",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Порада 7 — Упорядкувати програми",
  "usageTips.tip7.detail": "Утримуйте програму, перетягніть її всередині контейнера.",
  "usageTips.tip8.title": "Порада 8 — Редагувати нотатки",
  "usageTips.tip8.detail": "Клацніть програму правою кнопкою, введіть нотатку."
  "usageTips.tip8.detail": "Клацніть програму правою кнопкою, введіть нотатку.",
  "theme.default": "Типова",
  "theme.deepBlue": "Глибокий синій",
  "theme.black": "Чорна",
  "theme.pink": "Рожева",
  "theme.purple": "Фіолетова",
  "theme.green": "Зелена",
  "theme.blue": "Синя",
  "theme.colorful": "Барвиста"
}
src/Apptag/Localization/vi.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "Ngôn ngữ ứng dụng:",
  "settings.languageDesc": "Chọn ngôn ngữ dùng cho menu và cài đặt.",
  "settings.general": "Chung",
  "settings.theme": "Chủ đề",
  "settings.themeDesc": "Chọn chủ đề nền cho lưới ứng dụng.",
  "settings.themeScopeDesc": "Chủ đề chỉ thay đổi nền của lưới ứng dụng. Các khung giữ một bề mặt kính thống nhất.",
  "settings.tags": "Thẻ",
  "settings.data": "Dữ liệu",
  "settings.about": "Giới thiệu",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "Mẹo 7 - Sắp xếp ứng dụng",
  "usageTips.tip7.detail": "Nhấn giữ ứng dụng, kéo trong vùng chứa.",
  "usageTips.tip8.title": "Mẹo 8 - Chỉnh sửa ghi chú",
  "usageTips.tip8.detail": "Nhấp chuột phải vào ứng dụng, nhập ghi chú."
  "usageTips.tip8.detail": "Nhấp chuột phải vào ứng dụng, nhập ghi chú.",
  "theme.default": "Mặc định",
  "theme.deepBlue": "Xanh lam đậm",
  "theme.black": "Đen",
  "theme.pink": "Hồng",
  "theme.purple": "Tím",
  "theme.green": "Xanh lá",
  "theme.blue": "Xanh lam",
  "theme.colorful": "Nhiều màu"
}
src/Apptag/Localization/zh-Hans.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "应用语言:",
  "settings.languageDesc": "选择菜单和设置面板使用的语言。",
  "settings.general": "通用",
  "settings.theme": "主题",
  "settings.themeDesc": "选择应用网格的背景主题。",
  "settings.themeScopeDesc": "主题只改变应用网格背景。容器保持统一的玻璃面板。",
  "settings.tags": "标签",
  "settings.data": "数据",
  "settings.about": "关于",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "技巧7-应用排序",
  "usageTips.tip7.detail": "长按应用图标启动拖动 > 拖动到容器内的目标位置,放手即可",
  "usageTips.tip8.title": "技巧8-编辑备注",
  "usageTips.tip8.detail": "在应用上点击右键 > 输入内容"
  "usageTips.tip8.detail": "在应用上点击右键 > 输入内容",
  "theme.default": "默认",
  "theme.deepBlue": "深蓝",
  "theme.black": "黑色",
  "theme.pink": "粉色",
  "theme.purple": "紫色",
  "theme.green": "绿色",
  "theme.blue": "蓝色",
  "theme.colorful": "炫彩"
}
src/Apptag/Localization/zh-Hant.json
@@ -12,6 +12,9 @@
  "settings.languagePicker": "應用程式語言:",
  "settings.languageDesc": "選擇選單和設定面板使用的語言。",
  "settings.general": "一般",
  "settings.theme": "主題",
  "settings.themeDesc": "選擇應用程式網格的背景主題。",
  "settings.themeScopeDesc": "主題只會改變應用程式網格背景。容器會保持一致的玻璃面板。",
  "settings.tags": "標籤",
  "settings.data": "資料",
  "settings.about": "關於",
@@ -227,5 +230,13 @@
  "usageTips.tip7.title": "技巧7-應用排序",
  "usageTips.tip7.detail": "長按應用程式圖示開始拖曳 > 拖到容器內目標位置後放開",
  "usageTips.tip8.title": "技巧8-編輯備註",
  "usageTips.tip8.detail": "在應用程式上按右鍵 > 輸入內容"
  "usageTips.tip8.detail": "在應用程式上按右鍵 > 輸入內容",
  "theme.default": "預設",
  "theme.deepBlue": "深藍",
  "theme.black": "黑色",
  "theme.pink": "粉紅",
  "theme.purple": "紫色",
  "theme.green": "綠色",
  "theme.blue": "藍色",
  "theme.colorful": "炫彩"
}
src/Apptag/PreferencesView.swift
@@ -6,6 +6,7 @@
private enum SettingsTab: String, CaseIterable, Identifiable {
    case language
    case general
    case theme
    case hotkeys
    case tags
    case data
@@ -17,6 +18,7 @@
        switch self {
        case .language: return "settings.language"
        case .general: return "settings.general"
        case .theme: return "settings.theme"
        case .hotkeys: return "quickSearch.hotkeys"
        case .tags: return "settings.tags"
        case .data: return "settings.data"
@@ -28,6 +30,7 @@
        switch self {
        case .language: return "globe"
        case .general: return "gearshape"
        case .theme: return "paintpalette.fill"
        case .hotkeys: return "keyboard"
        case .tags: return "tag.fill"
        case .data: return "externaldrive.fill"
@@ -63,6 +66,7 @@
    @AppStorage("launchAtLogin") private var launchAtLogin = AppDefaults.launchAtLogin
    @AppStorage("showUncommonAppBubbles") private var showUncommonAppBubbles = AppDefaults.showUncommonAppBubbles
    @AppStorage("hideUsageTips") private var hideUsageTips = AppDefaults.hideUsageTips
    @AppStorage(AppGridTheme.storageKey) private var appGridThemeID = AppDefaults.appGridThemeID
    @AppStorage("mainHotkeyRegistrationState") private var mainHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue
    @AppStorage("quickSearchHotkeyRegistrationState") private var quickSearchHotkeyRegistrationState = LauncherHotkeyRegistrationState.active.rawValue
    @State private var selectedLanguage = L10n.selectedLanguageCode
@@ -79,6 +83,16 @@
    @State private var hotkeyStatusToastToken: UUID? = nil
    @State private var selectedTab: SettingsTab = .general
    @State private var selectedInitialLayoutMode: InitialLayoutMode = .smart
    init(initialTabRawValue: String? = nil) {
        let initialTab = initialTabRawValue.flatMap(SettingsTab.init(rawValue:)) ?? .general
        _selectedTab = State(initialValue: initialTab)
    }
    private func selectTab(rawValue: String?) {
        guard let rawValue, let tab = SettingsTab(rawValue: rawValue) else { return }
        selectedTab = tab
    }
    private func scanApps() {
        DispatchQueue.global(qos: .userInitiated).async {
@@ -514,6 +528,12 @@
            GridItem(.flexible(minimum: 220), spacing: 10, alignment: .top),
        ]
    }
    private var themeColumns: [GridItem] {
        [
            GridItem(.flexible(minimum: 220), spacing: 12, alignment: .top),
            GridItem(.flexible(minimum: 220), spacing: 12, alignment: .top),
        ]
    }
    private var displayModeOptions: [(id: String, title: String)] {
        [
            ("flat", tr("settings.flat")),
@@ -525,6 +545,9 @@
    }
    private var containerDisplayModeOptions: [(id: String, title: String)] {
        Array(displayModeOptions.dropFirst())
    }
    private var selectedAppGridTheme: AppGridTheme {
        AppGridTheme(storedID: appGridThemeID)
    }
    private func generalSettingRow<Control: View>(
@@ -833,6 +856,40 @@
                    .padding(.top, 28)
                    .padding(.bottom)
                }
                    case .theme:
            ScrollView(.vertical, showsIndicators: true) {
                VStack(alignment: .leading, spacing: 18) {
                    VStack(alignment: .leading, spacing: 6) {
                        Text(tr("settings.theme"))
                            .font(.headline)
                        Text(tr("settings.themeDesc"))
                            .font(.caption)
                            .foregroundStyle(.secondary)
                    }
                    LazyVGrid(columns: themeColumns, alignment: .leading, spacing: 12) {
                        ForEach(AppGridTheme.allCases) { theme in
                            ThemeOptionButton(
                                theme: theme,
                                title: tr(theme.titleKey),
                                isSelected: selectedAppGridTheme == theme
                            ) {
                                appGridThemeID = theme.rawValue
                            }
                        }
                    }
                    Text(tr("settings.themeScopeDesc"))
                        .font(.caption)
                        .foregroundStyle(.secondary)
                        .fixedSize(horizontal: false, vertical: true)
                }
                .frame(maxWidth: generalContentWidth, alignment: .leading)
                .padding(.horizontal)
                .padding(.top, 28)
                .padding(.bottom)
            }
                    case .hotkeys:
            // Tab 3: Hotkeys
@@ -1152,6 +1209,9 @@
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherHotkeyRegistrationChanged)) { _ in
            showPendingHotkeyWarningIfNeeded()
        }
        .onReceive(NotificationCenter.default.publisher(for: .tagLauncherPreferencesTabRequested)) { notification in
            selectTab(rawValue: notification.userInfo?[SettingsTabTarget.userInfoKey] as? String)
        }
    }
    private func showLanguageRefresh() {
@@ -1331,6 +1391,71 @@
    }
}
private struct ThemeOptionButton: View {
    let theme: AppGridTheme
    let title: String
    let isSelected: Bool
    let action: () -> Void
    var body: some View {
        Button(action: action) {
            HStack(spacing: 12) {
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .fill(
                        LinearGradient(
                            colors: theme.previewColors,
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                    )
                    .frame(width: 70, height: 42)
                    .overlay(
                        RoundedRectangle(cornerRadius: 8, style: .continuous)
                            .stroke(previewStrokeColor, lineWidth: 1)
                    )
                    .shadow(
                        color: isSelected ? Color.accentColor.opacity(0.22) : Color.clear,
                        radius: 10,
                        x: 0,
                        y: 4
                    )
                Text(title)
                    .font(.system(size: 13, weight: .semibold))
                    .foregroundStyle(.primary)
                    .lineLimit(1)
                    .minimumScaleFactor(0.82)
                Spacer(minLength: 0)
                Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
                    .font(.system(size: 16, weight: .semibold))
                    .foregroundStyle(isSelected ? Color.accentColor : Color.secondary.opacity(0.42))
            }
            .padding(10)
            .frame(maxWidth: .infinity, minHeight: 64)
            .background(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .fill(isSelected ? Color.accentColor.opacity(0.10) : Color(nsColor: .controlBackgroundColor))
            )
            .overlay(
                RoundedRectangle(cornerRadius: 8, style: .continuous)
                    .stroke(isSelected ? Color.accentColor.opacity(0.32) : Color.secondary.opacity(0.16), lineWidth: 1)
            )
            .contentShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
        }
        .buttonStyle(.plain)
        .accessibilityLabel(title)
        .accessibilityIdentifier("theme-option-\(theme.rawValue)")
    }
    private var previewStrokeColor: Color {
        theme.isDefaultLight
            ? Color.secondary.opacity(0.26)
            : Color.white.opacity(0.24)
    }
}
private struct DisplayModeOptionButton: View {
    let mode: String
    let title: String
src/Apptag/TagNavigationView.swift
@@ -21,6 +21,7 @@
    let appDragModeActive: Bool
    let appDropTargetID: String?
    let onActivate: (String) -> Void
    let onDoubleActivate: (String) -> Void
    let onHoverChange: (String, Bool) -> Void
    let canReorder: (String) -> Bool
    let canAcceptAppDrop: (String) -> Bool
@@ -41,6 +42,7 @@
            appDragModeActive: appDragModeActive,
            appDropTargetID: appDropTargetID,
            onActivate: onActivate,
            onDoubleActivate: onDoubleActivate,
            onHoverChange: onHoverChange,
            canReorder: canReorder,
            canAcceptAppDrop: canAcceptAppDrop,
@@ -63,6 +65,7 @@
            appDragModeActive: appDragModeActive,
            appDropTargetID: appDropTargetID,
            onActivate: onActivate,
            onDoubleActivate: onDoubleActivate,
            onHoverChange: onHoverChange,
            canReorder: canReorder,
            canAcceptAppDrop: canAcceptAppDrop,
@@ -100,6 +103,7 @@
        appDragModeActive: Bool,
        appDropTargetID: String?,
        onActivate: @escaping (String) -> Void,
        onDoubleActivate: @escaping (String) -> Void,
        onHoverChange: @escaping (String, Bool) -> Void,
        canReorder: @escaping (String) -> Bool,
        canAcceptAppDrop: @escaping (String) -> Bool,
@@ -118,6 +122,7 @@
            appDragModeActive: appDragModeActive,
            appDropTargetID: appDropTargetID,
            onActivate: onActivate,
            onDoubleActivate: onDoubleActivate,
            onHoverChange: onHoverChange,
            canReorder: canReorder,
            canAcceptAppDrop: canAcceptAppDrop,
@@ -170,6 +175,7 @@
    private var appDragModeActive = false
    private var appDropTargetID: String?
    private var onActivate: (String) -> Void = { _ in }
    private var onDoubleActivate: (String) -> Void = { _ in }
    private var onHoverChange: (String, Bool) -> Void = { _, _ in }
    private var canReorder: (String) -> Bool = { _ in false }
    private var canAcceptAppDrop: (String) -> Bool = { _ in false }
@@ -190,6 +196,7 @@
        appDragModeActive: Bool,
        appDropTargetID: String?,
        onActivate: @escaping (String) -> Void,
        onDoubleActivate: @escaping (String) -> Void,
        onHoverChange: @escaping (String, Bool) -> Void,
        canReorder: @escaping (String) -> Bool,
        canAcceptAppDrop: @escaping (String) -> Bool,
@@ -210,6 +217,7 @@
        self.appDragModeActive = appDragModeActive
        self.appDropTargetID = appDropTargetID
        self.onActivate = onActivate
        self.onDoubleActivate = onDoubleActivate
        self.onHoverChange = onHoverChange
        self.canReorder = canReorder
        self.canAcceptAppDrop = canAcceptAppDrop
@@ -261,6 +269,7 @@
        buttons = items.map { item in
            let button = TagNavigationButton(item: item, orientation: orientation)
            button.onActivate = { [weak self] tagID in self?.onActivate(tagID) }
            button.onDoubleActivate = { [weak self] tagID in self?.onDoubleActivate(tagID) }
            button.onHoverChange = { [weak self] tagID, active in self?.onHoverChange(tagID, active) }
            button.canReorder = { [weak self] tagID in self?.canReorder(tagID) ?? false }
            button.onReorderBegan = { [weak self] tagID in self?.onReorderBegan(tagID) }
@@ -348,6 +357,7 @@
final class TagNavigationButton: NSButton, AppDropTargetReceivingView {
    var onActivate: (String) -> Void = { _ in }
    var onDoubleActivate: (String) -> Void = { _ in }
    var onHoverChange: (String, Bool) -> Void = { _, _ in }
    var canReorder: (String) -> Bool = { _ in false }
    var onReorderBegan: (String) -> Void = { _ in }
@@ -475,6 +485,11 @@
    }
    override func mouseDown(with event: NSEvent) {
        if event.clickCount >= 2 {
            onDoubleActivate(item.id)
            return
        }
        guard canReorder(item.id) else {
            onActivate(item.id)
            return
src/CHANGELOG.md
@@ -1,5 +1,19 @@
# TagLauncher Changelog
## [8.0.1] — 2026-06-24
- 新增 App Grid 主题系统:设置页新增“主题”页签,可在默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题之间切换
- 默认主题保留原来的浅色毛玻璃 App Grid;黑色主题使用 100% 纯黑全屏背景;其余主题使用全屏高级渐变背景,单个容器继续保持统一半透明玻璃面板,不在容器内部叠加渐变
- 深蓝和黑色主题自动切换为深色玻璃容器;粉色、紫色、绿色、蓝色、炫彩使用更明亮的全屏渐变和浅色玻璃容器,保证图标、容器标题和标签导航的可读性
- 二次调色粉色、绿色、紫色、蓝色和炫彩主题:粉色更明亮柔和,绿色更接近草坪绿树后的毛玻璃,紫色/蓝色与深蓝拉开辨识度,炫彩按 Meilang logo 取色方向变亮
- 编辑模式做减法:无论当前选择哪个主题,进入编辑后都临时使用默认浅色毛玻璃 App Grid;退出编辑后自动恢复用户原主题,避免每个主题下编辑控件和文字可读性不一致
- 优化启动和重开 App Grid 的加载体验:新建面板会先复用最近一次完整 App 列表快照,并延迟显示空态转圈,减少冷启动或快速打开时的等待感
- 标签导航支持双击快速进入设置页的“标签”页签;单击滚动、hover 自动滚动和长按标签排序行为保持不变
- 旧版 `useDarkAppGrid` 偏好自动迁移:已启用旧深色视图的用户升级后会进入“深蓝”主题,未启用的用户继续保持默认浅色主题
- 补齐 29 个语种的主题页签、主题说明和 8 个主题名称文案;移除旧“启用深色视图”设置文案,避免设置页出现重复入口
- 新增 `Scripts/theme_settings_qa.sh`、`Scripts/appgrid_startup_loading_qa.sh` 和 `Scripts/tag_double_click_preferences_qa.sh`,检查主题枚举、设置入口、偏好迁移、App Grid 渲染边界、编辑模式默认浅色主题 override、启动加载体验、标签双击设置入口和 29 语种文案完整性
- 版本号更新为 `8.0.1`,Build 更新为 `20260624.2021`
## [7.9.4] — 2026-06-19
- 补修 Apple 自带应用默认备注迁移:覆盖 7.9 之前已写入用户库、但没有 metadata 的历史默认文案,修复 Chess、TV、Phone、Freeform、Music、Time Machine 等应用切换到非中文语言后仍显示旧中文备注的问题
src/Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md
New file
@@ -0,0 +1,41 @@
# 8.0.0 App Grid 主题系统 TODO
## 目标
- 在设置页新增独立“主题”页签。
- 提供 8 个 App Grid 背景主题:默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩。
- 主题只改变 App Grid 全屏背景渐变;单个容器继续使用统一半透明玻璃面板。
- 默认主题保持原浅色毛玻璃体验;深蓝和黑色使用深色玻璃容器,粉色、紫色、绿色、蓝色、炫彩使用浅色玻璃容器以保持明亮干净。
- 文案覆盖 29 个语种。
## 不做范围
- 不新增“容器玻璃深浅”独立设置。
- 不改变标签、拖拽、排序、Quick Search、数据导入导出、SmartStart 分类逻辑。
- 不改变默认浅色主题的既有视觉。
- 不在容器内部增加每个容器自己的渐变。
## TODO
- [x] 新增 `AppGridTheme` 统一主题模型。
- [x] 定义 8 个主题的背景渐变 token、预览色和容器玻璃适配策略。
- [x] 将 App Grid 背景渲染改为读取主题偏好。
- [x] 容器玻璃按主题明暗自动适配:深蓝/黑色为暗玻璃,亮色主题为浅玻璃。
- [x] 按用户视觉反馈二次调色:黑色改为纯黑背景;粉色、绿色、紫色、蓝色、炫彩改为更明亮且互相有辨识度的渐变。
- [x] 编辑模式做减法:进入编辑后临时使用默认浅色毛玻璃主题,退出后恢复用户原主题。
- [x] 设置页新增“主题”页签和 8 主题选择卡片。
- [x] 旧 `useDarkAppGrid` 偏好迁移到 `deepBlue`。
- [x] 补齐 29 个语种的主题文案。
- [x] 移除旧“启用深色视图”设置入口和本地化文案。
- [x] 新增主题设置 QA 脚本。
- [x] 更新 changelog、TODO 和工作日志。
- [ ] 用户视觉体验验收。
- [ ] 通过验收后再决定是否冻结、打包、创建正式 release tag。
## 完成标准
- 默认主题和 7.9.4 的浅色 App Grid 观感一致。
- 8 个主题切换只影响 App Grid 背景与自动玻璃适配,不影响功能行为。
- 所有 29 个语种都能显示主题页签和主题名称,不出现裸 key。
- 旧版已启用深色视图的用户升级后进入深蓝主题,未启用的用户保持默认主题。
- 构建、签名、macOS 14 metadata/typecheck、主题 QA、核心数据/搜索/使用技巧回归通过。
src/Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md
New file
@@ -0,0 +1,195 @@
# 8.0.0 App Grid 主题系统工作日志
## 2026-06-24
### 起点
- 分支:`codex/dark-glass-8.0.0`
- 基线:`v7.9.4-build20260619.2221`
- 目标:基于已认可方案实现 8 个高级渐变背景主题,并保留原浅色毛玻璃主题。
- 关键约束:主题切换只改 UI;不动标签、拖拽、排序、搜索、数据和 SmartStart 功能语义。
### 方案决策
- 8.0.0 先做“8 个高级渐变背景主题 + 自动玻璃适配”。
- 不做容器玻璃深浅的独立开关,降低设置复杂度和 QA 面。
- 默认主题使用浅色玻璃容器;深蓝和黑色使用深色半透明玻璃容器;粉色、紫色、绿色、蓝色、炫彩使用浅色玻璃容器,避免亮色主题被压成暗黑系。
- 渐变只在 App Grid 全屏背景层实现,单个容器内部不做独立渐变,避免视觉噪声。
- 炫彩主题取样自用户提供的 Meilang logo,使用青蓝、浅青、淡紫和粉色作为背景渐变来源,并降低饱和度保证可读性。
### 已完成实现
- 新增 `Apptag/AppGridTheme.swift`:
  - 主题枚举、存储 key、标题 key、背景渐变 token、预览色、玻璃适配策略。
- 修改 `AppDefaults.swift`:
  - 新增 `appGridThemeID` 默认值。
  - 将旧 `useDarkAppGrid=true` 迁移到 `deepBlue`。
- 修改 `ContentView.swift`:
  - App Grid 背景读取当前主题。
  - 默认主题保持原浅色视觉。
  - 黑色主题跳过毛玻璃背板,使用 100% 纯黑全屏背景。
  - 深蓝主题保留暗色渐变和轻暗化层。
  - 粉色、紫色、绿色、蓝色、炫彩使用更明亮的全屏渐变,不叠加暗化层。
- 修改 `AppGridCollectionView.swift`:
  - 容器、标题、分隔线按主题自动选择浅色/深色玻璃适配。
  - 深蓝和黑色为暗玻璃;亮色主题为浅玻璃。
  - 移除 AppKit 层旧的全屏背景渐变,避免 SwiftUI/AppKit 双背景叠加。
- 修改 `PreferencesView.swift`:
  - 新增“主题”tab。
  - 新增 8 个主题选择卡片和主题说明。
  - 移除 General 页旧“启用深色视图”入口。
- 更新 29 个 `Localization/*.json`:
  - 新增主题 tab、说明和 8 个主题名称。
  - 删除旧深色视图设置文案。
- 新增 `Scripts/theme_settings_qa.sh`。
- 新增根 `CODEGRAPH.md`,记录主题系统相关模块依赖。
### 2026-06-24 视觉反馈二次调色
- 用户反馈:默认和深蓝可接受;黑色需要纯黑;粉色和炫彩需要明亮、年轻;绿色需要像明亮草坪/绿树后的毛玻璃;紫色和蓝色需要与深蓝拉开差异。
- 调整结论:
  - 黑色:全屏背景为纯黑,不使用系统毛玻璃背板、不叠加渐变或暗化层。
  - 粉色:改为明亮粉、蜜桃和柔白高光,面向更年轻/少女取向。
  - 绿色:改为草坪绿、嫩绿和明亮高光,保持喜人而不沉闷。
  - 紫色:改为浅薰衣草到高饱和紫,和深蓝明显区分。
  - 蓝色:改为天空蓝、电光蓝和青色,和深蓝明显区分。
  - 炫彩:回到 Meilang logo 的青蓝、浅青、淡紫、亮粉取色方向,整体变亮。
  - 亮色主题统一改回浅色玻璃容器;深色玻璃仅保留给深蓝和黑色。
### QA 记录
- `bash build.sh`:PASS,生成 `src/build/TagLauncher.app`。
- `codesign --verify --deep --strict --verbose=2 src/build/TagLauncher.app`:PASS。
- 29 个 localization JSON 解析:PASS。
- `bash src/Scripts/theme_settings_qa.sh`:PASS,覆盖 8 主题、主题 tab、偏好迁移、纯黑策略、亮色主题浅玻璃策略和 29 语种文案。
- `bash src/Scripts/macos14_availability_typecheck_qa.sh`:PASS。
- `bash src/Scripts/macos14_build_metadata_qa.sh`:PASS。
- `bash src/Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。
- `bash src/Scripts/usage_tips_qa.sh`:PASS。
- `bash src/Scripts/app_ordering_data_qa.sh`:PASS。
- `bash src/Scripts/quick_search_system_app_qa.sh`:PASS。
- `bash src/Scripts/quick_search_app_name_qa.sh`:SKIP,本机未安装 QA fixture `/Applications/贝锐向日葵被控.app`。
### 2026-06-24 15:06 二次调色 QA 与预览包
- Build:`20260624.1506`
- `bash Scripts/theme_settings_qa.sh`:PASS。
- `bash Scripts/macos14_build_metadata_qa.sh`:PASS。
- `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
- `APP_BUILD=20260624.1506 bash build.sh`:PASS。
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。
- 构建产物版本:`8.0.0 (20260624.1506)`,`LSMinimumSystemVersion=14.0`。
- `bash Scripts/usage_tips_qa.sh`:PASS。
- `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。
- `bash Scripts/app_ordering_data_qa.sh`:PASS。
- `bash Scripts/quick_search_system_app_qa.sh`:PASS。
- `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。
- `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1506.dmg`:PASS。
- DMG:
  - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1506.dmg`
- SHA256:
  - `c2704c435ef0b0ba8e3ea17824cbd3ffe89c118f34a966547b98ab6db96ebac5`
### QA 基础设施备注
- `Scripts/window_logic_qa.sh` 在本机 GUI 自动化环境中出现间歇性失败:
  - Dock 点击或全局热键偶发未触发 overlay。
  - Codex/输入法类 App 偶发抢前台,导致旧的 frontmost app 名断言误报。
- 已对该脚本做两类非产品逻辑加固:
  - Quick Search 结果坐标识别不再依赖固定窗口宽度,并在失败时输出窗口列表。
  - 前台 app 名断言降级为诊断信息,核心仍由窗口层级断言覆盖。
  - 启动与热键触发增加重试等待。
- 该脚本仍建议后续单独作为 QA 基础设施任务继续稳定化;本轮主题功能不依赖其失败点。
### 当前结论
- 8.0.0 主题系统代码已实现。
- 确定性 QA 已覆盖主题模型、设置入口、偏好迁移、多语种文案、构建签名、macOS 14 元数据、标签 hover、使用技巧、App 排序数据和系统 Quick Search 名称回归。
- 下一步需要用户实际体验 8 个主题的视觉效果,再决定是否冻结和打包。
### 2026-06-24 15:53 编辑模式主题对比度修正
- 用户反馈:亮色主题和黑色主题进入编辑模式后,顶部按钮、说明文字、确认按钮、标签列表和页面文字可读性不足。
- 第一版修正策略:
  - 编辑层控件接入主题高对比 token,深色主题白字、亮色主题深字。
- Build 更新为:`20260624.1553`。
### 2026-06-24 15:53 QA 与预览包
- `bash Scripts/theme_settings_qa.sh`:PASS。
- `bash Scripts/macos14_build_metadata_qa.sh`:PASS。
- `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
- `APP_BUILD=20260624.1553 bash build.sh`:PASS。
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。
- `bash Scripts/usage_tips_qa.sh`:PASS。
- `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。
- `bash Scripts/app_ordering_data_qa.sh`:PASS。
- `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。
- `bash Scripts/quick_search_system_app_qa.sh`:PASS。
- 包内版本:`8.0.0 (20260624.1553)`,`LSMinimumSystemVersion=14.0`。
- `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1553.dmg`:PASS。
- DMG:
  - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1553.dmg`
- SHA256:
  - `543a2f26676f3e57aac0edb26353a8f90d38d3e0290dfd1c951e62b1f3e409ea`
### 2026-06-24 16:32 编辑模式主题策略简化
- 用户反馈:继续按主题适配编辑态仍然复杂,要求做减法。
- 新策略:
  - 用户进入编辑模式时,不管当前选择深蓝、黑色、粉色、绿色、炫彩或其他主题,App Grid 运行态渲染主题都临时切换为默认浅色毛玻璃。
  - 不写入 `appGridThemeID`,不改变设置页选择;退出编辑模式后自动恢复用户原主题。
  - App Grid 背景、AppKit collection renderer、编辑页顶部、标签列表、应用选择项都读取 `renderedAppGridTheme`。
  - `theme_settings_qa.sh` 增加运行态主题 override 检查,防止编辑态重新跟随用户主题。
- Build 更新为:`20260624.1632`。
### 2026-06-24 16:32 QA 与预览包
- `bash Scripts/theme_settings_qa.sh`:PASS。
- `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
- `APP_BUILD=20260624.1632 bash build.sh`:PASS。
- `bash Scripts/macos14_build_metadata_qa.sh`:PASS。
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。
- `bash Scripts/usage_tips_qa.sh`:PASS。
- `bash Scripts/tag_navigation_hover_scroll_qa.sh`:PASS。
- `bash Scripts/app_ordering_data_qa.sh`:PASS。
- `bash Scripts/quick_search_system_app_qa.sh`:PASS。
- `bash Scripts/apple_default_apps_resource_qa.sh`:PASS。
- 包内版本:`8.0.0 (20260624.1632)`,`LSMinimumSystemVersion=14.0`。
- `hdiutil verify build/TagLauncher-8.0.0-theme-preview-build20260624.1632.dmg`:PASS。
- DMG:
  - `src/build/TagLauncher-8.0.0-theme-preview-build20260624.1632.dmg`
- SHA256:
  - `34962ce1f3114a0b4979125fa92cd788833593ac19173365434c717bd8f159bc`
### 2026-06-24 17:31 App Grid 启动加载体验修复
- 用户反馈:每次启动/打开 App Grid 都出现转圈等待,观感不好,且此前类似问题曾修过。
- 根因结论:
  - App 启动时只预热 `AppIndexer` 的进程内扫描缓存。
  - 每次显示 App Grid 都会新建 `OverlayWindow + ContentView`,新的 `ContentView.allApps` 初始为空。
  - 在后台 `AppLibraryController.refresh()` 返回前,空态直接显示 `ProgressView`,因此用户能看到转圈。
  - 旧修复解决的是“缓存已热但新 ContentView 跳过 hydrate 导致 spinner 不消失”,没有解决冷启动或新面板首屏空态可见。
- 本轮修复:
  - `AppLibraryController` 保存最近一次完整 `AppLibrarySnapshot`。
  - `ContentView.refreshAppsForOverlay()` / `refreshAppsForQuickSearch()` 先复用最近快照,再后台刷新。
  - AppGrid 和编辑 AppGrid 的空态 spinner 改成 250ms 延迟显示;如果快照或扫描快速返回,不再闪现转圈。
  - 新增 `Scripts/appgrid_startup_loading_qa.sh` 锁住快照复用与延迟 spinner 规则。
### 2026-06-24 18:15 QA 与验收候选包
- 包类型:验收候选包,仅供用户安装验证;本轮未做源码冻结、commit 或 tag。
- Build 更新为:`20260624.1815`。
- `bash Scripts/theme_settings_qa.sh`:PASS。
- `bash Scripts/appgrid_startup_loading_qa.sh`:PASS。
- `bash Scripts/macos14_availability_typecheck_qa.sh`:PASS。
- `APP_BUILD=20260624.1815 bash build.sh`:PASS。
- `/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' build/TagLauncher.app/Contents/Info.plist`:`8.0.0`。
- `/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' build/TagLauncher.app/Contents/Info.plist`:`20260624.1815`。
- `codesign --verify --deep --strict --verbose=2 build/TagLauncher.app`:PASS。
- `bash Scripts/macos14_build_metadata_qa.sh`:PASS,`LSMinimumSystemVersion=14.0`,`minos=14.0`,`arches=arm64`。
- `hdiutil verify build/TagLauncher-8.0.0-build20260624.1815.dmg`:PASS。
- DMG:
  - `src/build/TagLauncher-8.0.0-build20260624.1815.dmg`
- SHA256:
  - `373355c87055680cb13e873bc5909abd84817d9ac2eaa98cc1ec2748be64bc80`
src/Scripts/appgrid_startup_loading_qa.sh
New file
@@ -0,0 +1,46 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$ROOT_DIR/Apptag"
python3 - "$APP_DIR" <<'PY'
import sys
from pathlib import Path
app_dir = Path(sys.argv[1])
content = (app_dir / "ContentView.swift").read_text(encoding="utf-8")
library = (app_dir / "AppLibraryController.swift").read_text(encoding="utf-8")
required_library_tokens = [
    "private static var cachedSnapshot: AppLibrarySnapshot?",
    "static func lastSnapshot() -> AppLibrarySnapshot?",
    "private static func updateLastSnapshot(_ snapshot: AppLibrarySnapshot)",
    "updateLastSnapshot(snapshot)",
]
for token in required_library_tokens:
    if token not in library:
        raise SystemExit(f"AppLibraryController missing startup snapshot token: {token}")
required_content_tokens = [
    "@State private var loadingSpinnerVisible = false",
    "@State private var loadingSpinnerToken = 0",
    "private let loadingSpinnerDelay: TimeInterval = 0.25",
    "hydrateFromLastAppLibrarySnapshotIfNeeded()",
    "AppLibraryController.lastSnapshot()",
    "scheduleLoadingSpinnerIfNeeded()",
    "hideLoadingSpinner()",
    "if loadingSpinnerVisible",
]
for token in required_content_tokens:
    if token not in content:
        raise SystemExit(f"ContentView missing startup loading token: {token}")
if "if allApps.isEmpty {\n                Spacer()\n                ProgressView().scaleEffect(0.8)" in content:
    raise SystemExit("AppGrid empty state still shows ProgressView immediately")
if "if allApps.isEmpty {\n                    Spacer(); ProgressView().scaleEffect(0.8); Spacer()" in content:
    raise SystemExit("edit AppGrid empty state still shows ProgressView immediately")
print("PASS AppGrid startup loading QA: last snapshot reuse and delayed spinner are present")
PY
src/Scripts/tag_double_click_preferences_qa.sh
New file
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TAG_NAV_SWIFT="$ROOT_DIR/Apptag/TagNavigationView.swift"
CONTENT_VIEW_SWIFT="$ROOT_DIR/Apptag/ContentView.swift"
APP_SWIFT="$ROOT_DIR/Apptag/ApptagApp.swift"
PREFERENCES_SWIFT="$ROOT_DIR/Apptag/PreferencesView.swift"
fail() {
  printf 'FAIL: %s\n' "$*" >&2
  exit 1
}
[[ -f "$TAG_NAV_SWIFT" ]] || fail "missing TagNavigationView.swift"
[[ -f "$CONTENT_VIEW_SWIFT" ]] || fail "missing ContentView.swift"
[[ -f "$APP_SWIFT" ]] || fail "missing ApptagApp.swift"
[[ -f "$PREFERENCES_SWIFT" ]] || fail "missing PreferencesView.swift"
python3 - "$TAG_NAV_SWIFT" "$CONTENT_VIEW_SWIFT" "$APP_SWIFT" "$PREFERENCES_SWIFT" <<'PY'
import pathlib
import re
import sys
tag_nav = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8")
content = pathlib.Path(sys.argv[2]).read_text(encoding="utf-8")
app = pathlib.Path(sys.argv[3]).read_text(encoding="utf-8")
prefs = pathlib.Path(sys.argv[4]).read_text(encoding="utf-8")
def fail(message: str) -> None:
    print(f"FAIL: {message}", file=sys.stderr)
    sys.exit(1)
def require(pattern: str, source: str, message: str) -> None:
    if not re.search(pattern, source, re.S):
        fail(message)
require(r"let\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "TagNavigationView must expose a double-activation callback")
require(r"private\s+var\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "document view must store the double-activation callback")
require(r"var\s+onDoubleActivate:\s*\(String\)\s*->\s*Void", tag_nav, "tag button must store the double-activation callback")
require(r"button\.onDoubleActivate\s*=\s*\{\s*\[weak\s+self\]\s*tagID\s+in\s+self\?\.onDoubleActivate\(tagID\)\s*\}", tag_nav, "tag button callback must be wired through the document view")
mouse_match = re.search(r"override\s+func\s+mouseDown\(with\s+event:\s+NSEvent\)\s*\{(?P<body>.*?)@objc\s+private\s+func\s+performActivation", tag_nav, re.S)
if not mouse_match:
    fail("TagNavigationButton.mouseDown must exist")
mouse_body = mouse_match.group("body")
double_click_index = mouse_body.find("event.clickCount >= 2")
reorder_guard_index = mouse_body.find("guard canReorder(item.id)")
long_press_index = mouse_body.find("longPressDuration: TimeInterval = 0.35")
if double_click_index < 0:
    fail("mouseDown must check for double-click")
if reorder_guard_index < 0:
    fail("mouseDown must keep the reorder guard")
if long_press_index < 0:
    fail("mouseDown must preserve the 0.35s long-press reorder threshold")
if not (double_click_index < reorder_guard_index < long_press_index):
    fail("double-click must be handled before entering reorder/long-press logic")
require(r"onDoubleActivate\(item\.id\)", mouse_body, "double-click must call onDoubleActivate with the tag id")
require(r"onActivate\(item\.id\)", mouse_body, "single-click activation must remain in mouseDown")
require(r"tagLauncherPreferencesTabRequested", content, "shared notification for settings tab switching must exist")
require(r"enum\s+SettingsTabTarget(?P<body>.*?)userInfoKey\s*=\s*\"tab\"(?P<body2>.*?)tags\s*=\s*\"tags\"", content, "settings tab target constants must exist")
require(r"onDoubleActivate:\s*\{\s*_\s+in\s*openTagSettingsFromNavigation\(\)\s*\}", content, "AppKit tag navigation must open tag settings on double-click")
require(r"private\s+func\s+openTagSettingsFromNavigation\(\)\s*\{(?P<body>.*?)tagLauncherOpenPreferencesRequested(?P<body2>.*?)SettingsTabTarget\.tags", content, "double-click helper must request Preferences Tags tab")
require(r"onActivate:\s*\{\s*tagID\s+in\s*activateTagNavigation\(tagID\)\s*\}", content, "single-click tag activation must still scroll")
require(r"handleTagNavigationHover\(tagID,\s*active:\s*active\)", content, "tag hover behavior must remain wired")
require(r"preferencesTabTarget\(from:\s*notification\)", app, "AppDelegate observer must read the requested preferences tab")
require(r"openPreferences\(targetTab:\s*Self\.preferencesTabTarget\(from:\s*notification\)\)", app, "preferences request must pass the target tab into openPreferences")
require(r"private\s+func\s+openPreferences\(targetTab:\s*String\?\)", app, "AppDelegate must have a target-tab preferences opener")
require(r"PreferencesView\(initialTabRawValue:\s*targetTab\)", app, "new settings windows must open on the requested tab")
require(r"requestPreferencesTabSelection\(targetTab\)", app, "existing settings windows must switch to the requested tab")
require(r"tagLauncherPreferencesTabRequested", app, "existing settings window tab switch must use the shared notification")
require(r"init\(initialTabRawValue:\s*String\?\s*=\s*nil\)", prefs, "PreferencesView must accept an initial tab")
require(r"SettingsTab\.init\(rawValue:\s*\)", prefs, "PreferencesView initial tab must be validated through SettingsTab raw values")
require(r"private\s+func\s+selectTab\(rawValue:\s*String\?\)", prefs, "PreferencesView must expose an internal tab-selection helper")
require(r"publisher\(for:\s*\.tagLauncherPreferencesTabRequested\)", prefs, "PreferencesView must listen for tab switch requests")
print("PASS: double-click tag navigation opens Preferences Tags tab without removing single-click, hover, or long-press wiring")
PY
src/Scripts/theme_settings_qa.sh
New file
@@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
APP_DIR="$ROOT_DIR/Apptag"
python3 - "$APP_DIR" <<'PY'
import json
import re
import sys
from pathlib import Path
app_dir = Path(sys.argv[1])
theme_file = app_dir / "AppGridTheme.swift"
preferences_file = app_dir / "PreferencesView.swift"
defaults_file = app_dir / "AppDefaults.swift"
content_file = app_dir / "ContentView.swift"
grid_file = app_dir / "AppGridCollectionView.swift"
localization_dir = app_dir / "Localization"
theme_source = theme_file.read_text(encoding="utf-8")
preferences_source = preferences_file.read_text(encoding="utf-8")
defaults_source = defaults_file.read_text(encoding="utf-8")
content_source = content_file.read_text(encoding="utf-8")
grid_source = grid_file.read_text(encoding="utf-8")
edit_views_source = (app_dir / "EditModeViews.swift").read_text(encoding="utf-8")
expected_cases = [
    "defaultLight",
    "deepBlue",
    "black",
    "pink",
    "purple",
    "green",
    "blue",
    "colorful",
]
for case in expected_cases:
    if not re.search(rf"case\s+{re.escape(case)}\b", theme_source):
        raise SystemExit(f"missing AppGridTheme case: {case}")
if "case theme" not in preferences_source:
    raise SystemExit("SettingsTab is missing the theme tab")
if "settings.darkAppGrid" in preferences_source:
    raise SystemExit("legacy dark grid copy is still referenced by PreferencesView")
if "Toggle(tr(\"settings.darkAppGrid\")" in preferences_source:
    raise SystemExit("legacy dark grid toggle is still visible")
if "useDarkAppGrid" in content_source or "useDarkAppGrid" in grid_source:
    raise SystemExit("legacy dark grid boolean still drives AppGrid rendering")
if "CAGradientLayer" in grid_source:
    raise SystemExit("AppGridCollectionView still owns a background gradient layer")
if "useDarkAppGrid" not in defaults_source or "AppGridTheme.deepBlue.rawValue" not in defaults_source:
    raise SystemExit("legacy dark preference migration is missing")
if "var usesVisualEffectBackdrop: Bool" not in theme_source:
    raise SystemExit("theme visual-effect backdrop policy is missing")
if "var isPureBlackBackground: Bool" not in theme_source or "self == .black" not in theme_source:
    raise SystemExit("pure black background policy is missing")
if "case .deepBlue, .black:" not in theme_source or "return true" not in theme_source:
    raise SystemExit("dark glass must be limited to deepBlue and black")
if "case .defaultLight, .pink, .purple, .green, .blue, .colorful:" not in theme_source or "return false" not in theme_source:
    raise SystemExit("bright themes must keep light glass")
if "backgroundDimmingOpacity" not in theme_source or "case .deepBlue:" not in theme_source:
    raise SystemExit("theme background dimming policy is missing")
if "theme.usesVisualEffectBackdrop" not in content_source or "theme.isPureBlackBackground" not in content_source:
    raise SystemExit("ContentView does not apply backdrop/pure-black theme policy")
required_edit_tokens = {
    "editPrimaryTextColor",
    "editSecondaryTextColor",
    "editTertiaryTextColor",
    "editDividerColor",
    "editToolbarSurfaceColor",
    "editControlSurfaceColor",
    "editControlStrokeColor",
    "editInactiveIndicatorColor",
    "editDisabledTextColor",
    "editDisabledSurfaceColor",
    "editButtonShadowColor",
    "editAccentColor",
    "editConfirmForegroundColor",
}
missing_tokens = [token for token in sorted(required_edit_tokens) if token not in theme_source]
if missing_tokens:
    raise SystemExit(f"AppGridTheme missing edit-mode contrast tokens: {missing_tokens}")
if "private var renderedAppGridTheme: AppGridTheme" not in content_source:
    raise SystemExit("ContentView is missing the runtime theme override")
if "editPhase == .none ? appGridTheme : .defaultLight" not in content_source:
    raise SystemExit("edit mode must temporarily render with the default light AppGrid theme")
if "let theme = renderedAppGridTheme" not in content_source:
    raise SystemExit("AppGrid background does not use the runtime rendered theme")
if "AppGridCollectionView(" in content_source and "appGridTheme: renderedAppGridTheme" not in content_source:
    raise SystemExit("AppGridCollectionView is not passed the runtime rendered theme")
if "EditAppsHeaderView(" in content_source and "theme: renderedAppGridTheme" not in content_source:
    raise SystemExit("edit apps header is not passed the runtime rendered theme")
if "theme: renderedAppGridTheme" not in content_source:
    raise SystemExit("edit mode selectable controls are not passed the runtime rendered theme")
for view_name in ["EditAppsHeaderView", "EditOperationPicker", "EditConfirmButton", "EditAppsSidebarIntroView", "EditSelectableTagItem", "EditableAppSelectionItem"]:
    if f"let theme: AppGridTheme" not in edit_views_source:
        raise SystemExit("edit mode views do not accept AppGridTheme")
    if view_name not in edit_views_source:
        raise SystemExit(f"missing edit mode view: {view_name}")
for token in ["theme.editPrimaryTextColor", "theme.editAccentColor", "theme.editControlSurfaceColor", "theme.editDisabledSurfaceColor"]:
    if token not in edit_views_source:
        raise SystemExit(f"edit mode views do not use contrast token: {token}")
if ".buttonStyle(.bordered)" in edit_views_source:
    raise SystemExit("edit mode views still rely on default bordered button styling")
if ".buttonStyle(.bordered)" in content_source and "editTagsView" in content_source:
    raise SystemExit("ContentView still relies on default bordered button styling in edit mode")
required_keys = {
    "settings.theme",
    "settings.themeDesc",
    "settings.themeScopeDesc",
    "theme.default",
    "theme.deepBlue",
    "theme.black",
    "theme.pink",
    "theme.purple",
    "theme.green",
    "theme.blue",
    "theme.colorful",
}
files = sorted(localization_dir.glob("*.json"))
if len(files) != 29:
    raise SystemExit(f"expected 29 localization files, found {len(files)}")
for path in files:
    data = json.loads(path.read_text(encoding="utf-8"))
    legacy = {"settings.darkAppGrid", "settings.darkAppGridDesc"} & set(data)
    if legacy:
        raise SystemExit(f"{path.name} still has legacy dark grid keys: {sorted(legacy)}")
    missing = required_keys - set(data)
    if missing:
        raise SystemExit(f"{path.name} missing keys: {sorted(missing)}")
    raw_values = [
        key for key in required_keys
        if str(data.get(key, "")).startswith("settings.") or str(data.get(key, "")).startswith("theme.")
    ]
    if raw_values:
        raise SystemExit(f"{path.name} has raw key values: {raw_values}")
print("PASS theme settings QA: 8 themes, Theme tab, migration, AppGrid-only rendering, glass policy, edit-mode default-theme override, and 29-language keys are present")
PY
src/Scripts/window_logic_qa.sh
@@ -411,9 +411,10 @@
  local frontmost
  frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')"
  if [[ "$frontmost" != "TagLauncher" ]]; then
    echo "FAIL: frontmost app is $frontmost, expected TagLauncher" >&2
    return 1
    log "INFO frontmost app is $frontmost; relying on TagLauncher window-layer assertions for this headless QA run"
    return 0
  fi
  log "PASS frontmost app: TagLauncher"
}
prepare_isolated_app_instance() {
@@ -427,7 +428,7 @@
  open -n "$APP_BUNDLE"
  sleep 1.0
  dismiss_reopen_dialog
  sleep 1.5
  sleep 2.0
  if ! is_qa_app_only_running; then
    echo "FAIL: expected only QA build TagLauncher instance to be running" >&2
@@ -790,6 +791,39 @@
    return 0
}
func rect(_ window: [String: Any]) -> CGRect {
    let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:]
    return CGRect(
        x: dimension(bounds, "X"),
        y: dimension(bounds, "Y"),
        width: dimension(bounds, "Width"),
        height: dimension(bounds, "Height")
    )
}
func isOverlayWindow(_ window: [String: Any]) -> Bool {
    let name = (window[kCGWindowName as String] as? String) ?? ""
    guard name.isEmpty else { return false }
    let windowFrame = rect(window)
    return NSScreen.screens.contains { screen in
        let screenFrame = screen.frame
        return abs(windowFrame.width - screenFrame.width) <= 12
            && windowFrame.height >= screenFrame.height * 0.75
            && abs(windowFrame.midX - screenFrame.midX) <= 12
    }
}
func dumpTagWindows() {
    fputs("---- TagLauncher windows for coords ----\n", stderr)
    for (index, window) in tag.enumerated() {
        let name = (window[kCGWindowName as String] as? String) ?? ""
        let layer = window[kCGWindowLayer as String] as? Int ?? -999
        let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:]
        fputs("#\(index) name=\(name) layer=\(layer) bounds=\(bounds)\n", stderr)
    }
    fputs("----------------------------------------\n", stderr)
}
switch mode {
case "data-tab":
    guard let settings = tag.first(where: { (($0[kCGWindowName as String] as? String) ?? "").isEmpty == false }),
@@ -842,24 +876,30 @@
    let overlayHeight = dimension(overlayBounds, "Height")
    print("\(Int(round(overlayX + overlayWidth / 2))) \(Int(round(overlayY + overlayHeight / 2)))")
case "quick-search-result":
    guard let quickSearch = tag.first(where: { window in
    let candidates = tag.filter { window in
        let name = (window[kCGWindowName as String] as? String) ?? ""
        let bounds = window[kCGWindowBounds as String] as? NSDictionary ?? [:]
        let width = dimension(bounds, "Width")
        let height = dimension(bounds, "Height")
        return name.isEmpty
            && width >= 500
            && width <= 900
            && height >= 120
            && height <= 850
    }), let bounds = quickSearch[kCGWindowBounds as String] as? NSDictionary else {
            && !isOverlayWindow(window)
            && width >= 360
            && width <= ((NSScreen.screens.first?.frame.width ?? 1600) * 0.95)
            && height >= 90
            && height <= 900
    }
    guard let quickSearch = candidates.min(by: { rect($0).width * rect($0).height < rect($1).width * rect($1).height }),
          let bounds = quickSearch[kCGWindowBounds as String] as? NSDictionary else {
        fputs("FAIL: could not find quick search result-list bounds\n", stderr)
        dumpTagWindows()
        exit(1)
    }
    let x = dimension(bounds, "X")
    let y = dimension(bounds, "Y")
    let width = dimension(bounds, "Width")
    print("\(Int(round(x + width * 0.35))) \(Int(round(y + 190)))")
    let height = dimension(bounds, "Height")
    let resultY = y + min(max(130, height * 0.55), max(80, height - 35))
    print("\(Int(round(x + width * 0.35))) \(Int(round(resultY)))")
case "fullscreen-target-center":
    guard let target = raw.first(where: { ($0[kCGWindowName as String] as? String) == "TagLauncherFullscreenQATargetFullscreen" }),
          let bounds = target[kCGWindowBounds as String] as? NSDictionary else {
@@ -1075,7 +1115,7 @@
open_quick_search_with_retry() {
  local output=""
  for _ in {1..3}; do
  for _ in {1..6}; do
    send_quick_search_hotkey
    sleep 0.8
    if output="$(wait_swift_assert quick-search 2>&1)"; then
@@ -1093,6 +1133,20 @@
    send_keycode 49
    sleep 0.4
    if output="$(wait_swift_assert quick-search 2>&1)"; then
      printf '%s\n' "$output"
      return 0
    fi
  done
  printf '%s\n' "$output" >&2
  return 1
}
open_overlay_with_retry() {
  local output=""
  for _ in {1..3}; do
    send_main_hotkey
    sleep 0.6
    if output="$(wait_swift_assert overlay 2>&1)"; then
      printf '%s\n' "$output"
      return 0
    fi
@@ -1307,9 +1361,7 @@
defaults write "$DEFAULTS_DOMAIN" showDockIcon -bool false
prepare_isolated_app_instance
assert_no_dock_tile
send_main_hotkey
sleep 0.8
wait_swift_assert overlay
open_overlay_with_retry
assert_no_dock_tile
send_keycode 53
sleep 0.4
@@ -1384,8 +1436,7 @@
log "==> QA 1/7: overlay claims foreground, hides Dock, keeps menu bar visible"
show_overlay
frontmost="$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true')"
[[ "$frontmost" == "TagLauncher" ]] || { echo "FAIL: frontmost app is $frontmost, expected TagLauncher" >&2; exit 1; }
assert_frontmost_taglauncher
swift_assert overlay
log "==> QA 1/7 and 4/7: settings floats above appgrid and quick search"
src/TODO.md
@@ -11,6 +11,13 @@
## In Progress
- [2026-06-24] 8.0.0 App Grid 主题系统。
  - 目标: 在设置页新增“主题”页签,提供默认、深蓝、黑色、粉色、紫色、绿色、蓝色、炫彩 8 个主题;主题只改变 App Grid 全屏背景渐变,容器保持统一半透明玻璃面板,并按主题明暗自动适配玻璃明暗。
  - 范围: App Grid 背景、容器玻璃适配、设置页主题选择、旧 `useDarkAppGrid` 偏好迁移、29 语种主题文案、主题 QA 脚本。
  - 不做: 不新增容器玻璃深浅独立开关;不改标签/拖拽/排序/Quick Search/数据结构功能语义;不改浅色默认主题视觉。
  - 当前状态: 已完成代码实现、29 语种文案和确定性 QA;已按用户反馈将黑色改为纯黑,并将粉色/绿色/紫色/蓝色/炫彩改为更明亮且有辨识度的版本;等待用户视觉体验验收后决定是否继续冻结/打包。
  - 文档: `Docs/Requirements/2026-06-24-app-grid-theme-system-todo.md`、`Docs/Requirements/2026-06-24-app-grid-theme-system-worklog.md`。
## Todo
- [发布后独立任务] 设置页 tab 容器 AppKit/自控化评估与实现。