Ariver
2026-05-18 a5cba8fa3153098d95acb20b6060fac704ecc489
Apptag/TagGroupView.swift
@@ -1,4 +1,5 @@
import SwiftUI
import UniformTypeIdentifiers
// MARK: - Tag Group Section (with centered separator-line header)
@@ -8,10 +9,17 @@
    let tagFontSize: CGFloat
    let iconSize: CGFloat
    var showNames: Bool = true
    var dragModeActive: Bool = false
    var onDragModeChange: ((Bool) -> Void)? = nil
    var onBubbleHover: ((AppInfo, CGRect, Bool) -> Void)? = nil
    var onEditNote: ((AppInfo, CGRect) -> Void)? = nil
    @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
@@ -45,10 +53,54 @@
                        app: app,
                        iconSize: iconSize,
                        showName: showNames,
                        sourceTag: group.name,
                        dragModeActive: dragModeActive,
                        onDragModeChange: onDragModeChange,
                        onBubbleHover: onBubbleHover,
                        onEditNote: onEditNote,
                        itemID: "\(group.name)|\(app.path.path)",
                        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
    }
}