spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
1//2// TabBarView.swift3// Zyquo Atlas4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Top horizontal tab strip. Each tab shows its title (and a spinner while9// loading), the active tab is tinted with the accent, and hovering reveals a10// close button. Phase 4 adds the vertical/left layout option, drag-reorder,11// pinning, and hover previews; this is the top-bar baseline.12//1314import SwiftUI1516struct TabBarView: View {17 @ObservedObject var tabManager: TabManager18 @EnvironmentObject private var themeEngine: ThemeEngine19 private var theme: AtlasTheme { themeEngine.theme }2021 var body: some View {22 ScrollView(.horizontal, showsIndicators: false) {23 HStack(spacing: ZyquoSpacing.xxs) {24 ForEach(tabManager.tabs) { tab in25 TabChip(26 tab: tab,27 isActive: tab.id == tabManager.activeTabID,28 onSelect: { tabManager.selectTab(tab.id) },29 onClose: { tabManager.closeTab(tab.id) },30 onTogglePin: { tabManager.togglePin(tab.id) }31 )32 }33 }34 .padding(.horizontal, ZyquoSpacing.xs)35 }36 .frame(height: ZyquoMetrics.tabBarHeight)37 .background(theme.backgroundColor)38 }39}4041private struct TabChip: View {42 @ObservedObject var tab: Tab43 let isActive: Bool44 let onSelect: () -> Void45 let onClose: () -> Void46 var onTogglePin: () -> Void = {}47 @EnvironmentObject private var themeEngine: ThemeEngine48 private var theme: AtlasTheme { themeEngine.theme }4950 @State private var hovering = false5152 /// Pinned tabs are compact (icon only); suspended tabs dim.53 private var isCompact: Bool { tab.isPinned }5455 var body: some View {56 HStack(spacing: ZyquoSpacing.xs) {57 leading58 .frame(width: 14, height: 14)5960 if !isCompact {61 Text(tab.title)62 .font(ZyquoFont.control)63 .lineLimit(1)64 .foregroundStyle(isActive ? theme.textPrimary : theme.textSecondary)6566 Spacer(minLength: 0)6768 if hovering || isActive {69 Button(action: onClose) {70 Image(systemName: "xmark")71 .font(.system(size: 9, weight: .bold))72 .frame(width: 16, height: 16)73 .contentShape(Rectangle())74 }75 .buttonStyle(.plain)76 .foregroundStyle(theme.textTertiary)77 }78 }79 }80 .padding(.horizontal, ZyquoSpacing.sm)81 .frame(width: isCompact ? 44 : ZyquoMetrics.tabMinWidth, height: ZyquoMetrics.tabBarHeight - 6)82 .opacity(tab.isSuspended && !isActive ? 0.6 : 1)83 .background(84 RoundedRectangle(cornerRadius: ZyquoRadius.small)85 .fill(isActive ? theme.accentSubtle86 : (hovering ? theme.surfaceSecondary : .clear))87 )88 .overlay(alignment: .bottom) {89 if isActive {90 Rectangle().fill(theme.accent).frame(height: 2)91 .padding(.horizontal, ZyquoSpacing.xs)92 }93 }94 .contentShape(Rectangle())95 .onTapGesture(perform: onSelect)96 .onHover { hovering = $0 }97 .help(tab.title)98 .contextMenu {99 Button(tab.isPinned ? "Unpin Tab" : "Pin Tab", action: onTogglePin)100 Button("Close Tab", action: onClose)101 }102 }103104 @ViewBuilder105 private var leading: some View {106 if tab.isLoading {107 ProgressView()108 .controlSize(.mini)109 .scaleEffect(0.7)110 } else {111 Image(systemName: "globe")112 .font(.system(size: 10))113 .foregroundStyle(theme.textTertiary)114 }115 }116}117