diff --git a/ora/Modules/Sidebar/ContainerView.swift b/ora/Modules/Sidebar/ContainerView.swift index 4791cf04..bde74ed1 100644 --- a/ora/Modules/Sidebar/ContainerView.swift +++ b/ora/Modules/Sidebar/ContainerView.swift @@ -11,11 +11,12 @@ struct ContainerView: View { @EnvironmentObject var privacyMode: PrivacyMode @State var isDragging = false + @State private var isHovered = false @State private var draggedItem: UUID? @State private var editingURLString: String = "" var body: some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading, spacing: 0) { if toolbarManager.isToolbarHidden, let tab = tabManager.activeTab { SidebarURLDisplay( tab: tab, @@ -73,7 +74,27 @@ struct ContainerView: View { onMoveToContainer: moveTab, containers: containers ) - Divider() + + HStack { + ZStack { + Capsule() + .frame(height: 1) + Text("Clear").bold().font(.footnote).opacity(0) + } + if isHovered, !normalTabs.isEmpty { + Button("Clear") { + withAnimation { + for tab in container.tabs where tab.type == .normal { + tabManager.closeTab(tab: tab) + } + } + } + .bold() + .font(.footnote) + .buttonStyle(.plain) + } + } + .foregroundStyle(.secondary) } NormalTabsList( tabs: normalTabs, @@ -91,6 +112,11 @@ struct ContainerView: View { } } .modifier(OraWindowDragGesture(isDragging: $isDragging)) + .onHover { hov in + withAnimation { + isHovered = hov + } + } } private var favoriteTabs: [Tab] { diff --git a/ora/Modules/Sidebar/SidebarView.swift b/ora/Modules/Sidebar/SidebarView.swift index c1eb47da..6eca875c 100644 --- a/ora/Modules/Sidebar/SidebarView.swift +++ b/ora/Modules/Sidebar/SidebarView.swift @@ -47,7 +47,7 @@ struct SidebarView: View { } var body: some View { - VStack(alignment: .leading, spacing: 16) { + VStack(alignment: .leading) { SidebarToolbar() NSPageView( selection: selectedContainerIndex, diff --git a/ora/Modules/Sidebar/TabList/FavTabsList.swift b/ora/Modules/Sidebar/TabList/FavTabsList.swift index 68c095c0..4ff88b84 100644 --- a/ora/Modules/Sidebar/TabList/FavTabsList.swift +++ b/ora/Modules/Sidebar/TabList/FavTabsList.swift @@ -4,6 +4,8 @@ import SwiftUI struct FavTabsGrid: View { @Environment(\.theme) var theme @EnvironmentObject var tabManager: TabManager + @EnvironmentObject var sidebarManager: SidebarManager + @State private var isHoveringOverEmpty: Bool = false let tabs: [Tab] @Binding var draggedItem: UUID? let onDrag: (UUID) -> NSItemProvider @@ -25,18 +27,28 @@ struct FavTabsGrid: View { } var body: some View { - LazyVGrid(columns: adaptiveColumns, spacing: 10) { + let isShowingHidden = tabs.isEmpty && !( + sidebarManager.stickyFavs || isHoveringOverEmpty + ) + LazyVGrid(columns: adaptiveColumns, spacing: isShowingHidden ? 0 : 10) { if tabs.isEmpty { - EmptyFavTabItem() - .onDrop( - of: [.text], - delegate: SectionDropDelegate( - items: tabs, - draggedItem: $draggedItem, - targetSection: .fav, - tabManager: tabManager - ) + Group { + if sidebarManager.stickyFavs || isHoveringOverEmpty { + EmptyFavTabItem() + } else { + Capsule().frame(height: 3).opacity(0) + } + } + .onDrop( + of: [.text], + delegate: SectionDropDelegate( + items: tabs, + draggedItem: $draggedItem, + targetSection: .fav, + tabManager: tabManager, + isHovering: $isHoveringOverEmpty ) + ) } else { ForEach(tabs) { tab in FavTabItem( @@ -71,5 +83,10 @@ struct FavTabsGrid: View { tabManager: tabManager ) ) + .onChange(of: tabs.count) { _, newTabs in + if newTabs > 0 { + sidebarManager.stickyFavs = false + } + } } } diff --git a/ora/Modules/Sidebar/TabList/PinnedTabsList.swift b/ora/Modules/Sidebar/TabList/PinnedTabsList.swift index 63b16ab1..729d67c4 100644 --- a/ora/Modules/Sidebar/TabList/PinnedTabsList.swift +++ b/ora/Modules/Sidebar/TabList/PinnedTabsList.swift @@ -4,6 +4,7 @@ import SwiftUI struct PinnedTabsList: View { let tabs: [Tab] @Binding var draggedItem: UUID? + @State private var isHoveringOverEmpty = false let onDrag: (UUID) -> NSItemProvider let onSelect: (Tab) -> Void let onPinToggle: (Tab) -> Void @@ -13,17 +14,31 @@ struct PinnedTabsList: View { let onMoveToContainer: (Tab, TabContainer) -> Void let containers: [TabContainer] @EnvironmentObject var tabManager: TabManager + @EnvironmentObject var sidebarManager: SidebarManager @Environment(\.theme) var theme + private var spaceName: String? { + if let active = tabManager.activeContainer { + return "\(active.emoji) \(active.name)" + } + return nil + } + var body: some View { VStack(spacing: 8) { - Text("Pinned") + Text(spaceName ?? "Pinned") .font(.callout) .foregroundColor(theme.mutedForeground) .padding(.top, 8) .frame(maxWidth: .infinity, alignment: .leading) if tabs.isEmpty { - EmptyPinnedTabs() + Group { + if sidebarManager.stickyPinned || isHoveringOverEmpty { + EmptyPinnedTabs() + } else { + Capsule().frame(height: 3).opacity(0) + } + } } else { ForEach(tabs) { tab in TabItem( @@ -57,8 +72,14 @@ struct PinnedTabsList: View { items: tabs, draggedItem: $draggedItem, targetSection: .pinned, - tabManager: tabManager + tabManager: tabManager, + isHovering: $isHoveringOverEmpty ) ) + .onChange(of: tabs.count) { _, new in + if new > 0 { + sidebarManager.stickyPinned = false + } + } } } diff --git a/ora/Services/SectionDropDelegate.swift b/ora/Services/SectionDropDelegate.swift index e6a70639..e4aa55c5 100644 --- a/ora/Services/SectionDropDelegate.swift +++ b/ora/Services/SectionDropDelegate.swift @@ -4,11 +4,39 @@ import SwiftUI struct SectionDropDelegate: DropDelegate { let items: [Tab] @Binding var draggedItem: UUID? + @Binding var isHovering: Bool let targetSection: TabSection let tabManager: TabManager + init( + items: [Tab], + draggedItem: Binding, + targetSection: TabSection, + tabManager: TabManager, + isHovering: Binding? = nil + ) { + self.items = items + self._draggedItem = draggedItem + self.targetSection = targetSection + self.tabManager = tabManager + self._isHovering = isHovering ?? .constant(false) + } + func dropEntered(info: DropInfo) { - guard let provider = info.itemProviders(for: [.text]).first else { return } + isHovering = true + } + + func dropExited(info: DropInfo) { + isHovering = false + } + + func dropUpdated(info: DropInfo) -> DropProposal? { + DropProposal(operation: .move) + } + + func performDrop(info: DropInfo) -> Bool { + isHovering = false + guard let provider = info.itemProviders(for: [.text]).first else { return false } performHapticFeedback(pattern: .alignment) provider.loadObject(ofClass: NSString.self) { object, _ in @@ -48,13 +76,6 @@ struct SectionDropDelegate: DropDelegate { // } } } - } - - func dropUpdated(info: DropInfo) -> DropProposal? { - DropProposal(operation: .move) - } - - func performDrop(info: DropInfo) -> Bool { draggedItem = nil return true } diff --git a/ora/Services/SidebarManager.swift b/ora/Services/SidebarManager.swift index 8ac6eb1e..92e33789 100644 --- a/ora/Services/SidebarManager.swift +++ b/ora/Services/SidebarManager.swift @@ -7,6 +7,8 @@ enum SidebarPosition: String, Hashable { @MainActor class SidebarManager: ObservableObject { + @AppStorage("ui.sidebar.favorites.sticky") var stickyFavs: Bool = true + @AppStorage("ui.sidebar.pinned.sticky") var stickyPinned: Bool = true @AppStorage("ui.sidebar.hidden") var isSidebarHidden: Bool = false @AppStorage("ui.sidebar.position") var sidebarPosition: SidebarPosition = .primary