SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
3.9 KB · 117 lines swift
Raw Blame History
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