From f8e102d2756a56efb6b9d92e578aedc3b7d63084 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Wed, 20 May 2026 12:48:40 +0800
Subject: [PATCH] checkpoint: archive current 7.5.1 before performance optimization

---
 Apptag/TagGroupView.swift |   60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 files changed, 59 insertions(+), 1 deletions(-)

diff --git a/Apptag/TagGroupView.swift b/Apptag/TagGroupView.swift
index 1089136..3220b93 100644
--- a/Apptag/TagGroupView.swift
+++ b/Apptag/TagGroupView.swift
@@ -1,4 +1,5 @@
 import SwiftUI
+import UniformTypeIdentifiers
 
 // MARK: - Tag Group Section (with centered separator-line header)
 
@@ -7,10 +8,20 @@
     let onSelectApp: (AppInfo) -> Void
     let tagFontSize: CGFloat
     let iconSize: CGFloat
+    var showNames: Bool = true
+    var dragModeActive: Bool = false
+    var onDragModeChange: ((Bool) -> Void)? = nil
+    var onBubbleHover: ((AppInfo, CGRect, AppBubbleHoverEvent) -> Void)? = nil
+    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
+    var bubbleDisabled: Bool = false
+    var dragResetToken: Int = 0
+    @Binding var hoveredAppItemID: String?
+    var onDropApp: ((String, String, Bool) -> Void)? = nil
 
     /// Adaptive columns — auto-fit based on icon size and available width.
     private var columns: [GridItem] {
-        [GridItem(.adaptive(minimum: iconSize + 28, maximum: iconSize + 64), spacing: 6)]
+        let itemWidth = AppGridItem.stableWidth(iconSize: iconSize)
+        return [GridItem(.adaptive(minimum: itemWidth, maximum: itemWidth + 36), spacing: 6)]
     }
 
     // Subtle separator line color — adapts to light/dark mode
@@ -43,10 +54,57 @@
                     AppGridItem(
                         app: app,
                         iconSize: iconSize,
+                        showName: showNames,
+                        sourceTag: group.name,
+                        dragModeActive: dragModeActive,
+                        onDragModeChange: onDragModeChange,
+                        onBubbleHover: onBubbleHover,
+                        onEditNote: onEditNote,
+                        bubbleDisabled: bubbleDisabled,
+                        itemID: "\(group.name)|\(app.path.path)",
+                        dragResetToken: dragResetToken,
+                        hoveredAppItemID: $hoveredAppItemID,
                         onSelect: { onSelectApp(app) }
                     )
                 }
             }
         }
+        .contentShape(Rectangle())
+        .overlay {
+            AppDropTargetView(targetTag: group.name) { path, source, copy in
+                onDropApp?(path, source, copy)
+            }
+            .allowsHitTesting(false)
+        }
+        .onDrop(of: [UTType.plainText], isTargeted: nil) { providers in
+            handleDrop(providers)
+        }
+    }
+
+    private func handleDrop(_ providers: [NSItemProvider]) -> Bool {
+        guard let provider = providers.first(where: { $0.hasItemConformingToTypeIdentifier(UTType.plainText.identifier) }) else {
+            return false
+        }
+        provider.loadItem(forTypeIdentifier: UTType.plainText.identifier, options: nil) { item, _ in
+            let text: String?
+            if let data = item as? Data {
+                text = String(data: data, encoding: .utf8)
+            } else if let string = item as? String {
+                text = string
+            } else if let string = item as? NSString {
+                text = string as String
+            } else {
+                text = nil
+            }
+            guard let text else { return }
+            let parts = text.components(separatedBy: "\n")
+            guard let path = parts.first, !path.isEmpty else { return }
+            let source = parts.dropFirst().first ?? ""
+            let copy = NSEvent.modifierFlags.contains(.option)
+            DispatchQueue.main.async {
+                onDropApp?(path, source, copy)
+            }
+        }
+        return true
     }
 }

--
Gitblit v1.9.3