// // TabSwitcherView.swift // Prisme // // Author: Simon-Pierre Boucher // import SwiftUI /// Tab overview, sectioned by identity container. Intention-based grouping /// (P0) will be *proposed* on top of this later — never imposed. struct TabSwitcherView: View { let model: BrowserModel @Environment(\.dismiss) private var dismiss @State private var showHistory = false private let columns = [GridItem(.adaptive(minimum: 160), spacing: Spacing.m)] var body: some View { NavigationStack { Group { if model.tabs.tabs.isEmpty { ContentUnavailableView( "Aucun onglet", systemImage: "square.on.square", description: Text("Touchez + pour ouvrir un onglet dans l'univers actif.") ) } else { grid } } .navigationTitle("Onglets") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("OK") { dismiss() } .fontWeight(.semibold) } ToolbarItem(placement: .primaryAction) { Button { model.newTab() dismiss() } label: { Image(systemName: "plus") } } ToolbarItem(placement: .secondaryAction) { Button { showHistory = true } label: { Label("Historique", systemImage: "clock.arrow.circlepath") } .accessibilityIdentifier("tabs.history") } } .sheet(isPresented: $showHistory) { HistoryView( history: model.history, containers: model.containers, onOpen: { visit in model.open(visit) dismiss() } ) } } } private var grid: some View { ScrollView { LazyVGrid(columns: columns, alignment: .leading, spacing: Spacing.m) { ForEach(model.containers.all) { container in let tabs = model.tabs.tabs(in: container.id) if !tabs.isEmpty { Section { ForEach(tabs) { tab in TabCard( tab: tab, container: container, isActive: tab.id == model.tabs.activeTabID, onSelect: { model.activate(tab) dismiss() }, onClose: { withAnimation(.spring(duration: 0.3)) { model.tabs.close(tab) } } ) .transition(.scale(scale: 0.85).combined(with: .opacity)) } } header: { HStack(spacing: Spacing.s) { Image(systemName: container.symbol) .font(.subheadline.weight(.semibold)) Text(container.name) .font(.headline) Text("\(tabs.count)") .font(.subheadline.weight(.semibold)) .foregroundStyle(.secondary) } .foregroundStyle(container.color.color) .padding(.top, Spacing.l) } } } } .padding(Spacing.l) } } } // MARK: - Card private struct TabCard: View { let tab: Tab let container: IdentityContainer let isActive: Bool let onSelect: () -> Void let onClose: () -> Void var body: some View { Button(action: onSelect) { VStack(alignment: .leading, spacing: 0) { // Container-colored crown. Rectangle() .fill(container.color.gradient) .frame(height: 5) VStack(alignment: .leading, spacing: Spacing.s) { HStack(alignment: .top) { siteBadge Spacer(minLength: Spacing.s) Button(action: onClose) { Image(systemName: "xmark") .font(.caption.bold()) .foregroundStyle(.secondary) .frame(width: 26, height: 26) .background(.quaternary, in: Circle()) .contentShape(Circle()) } .buttonStyle(.plain) } Text(tab.displayTitle) .font(.subheadline.weight(.semibold)) .lineLimit(2) .multilineTextAlignment(.leading) .foregroundStyle(.primary) if let host = tab.page.url?.host() { Text(host) .font(.caption) .foregroundStyle(.secondary) .lineLimit(1) } } .padding(Spacing.m) } .frame(maxWidth: .infinity, minHeight: 118, alignment: .topLeading) .background(.regularMaterial, in: RoundedRectangle(cornerRadius: Radius.l)) .overlay { RoundedRectangle(cornerRadius: Radius.l) .strokeBorder( isActive ? AnyShapeStyle(container.color.color) : AnyShapeStyle(.quaternary), lineWidth: isActive ? 2.5 : 1 ) } .clipShape(RoundedRectangle(cornerRadius: Radius.l)) .shadow(color: .black.opacity(isActive ? 0.14 : 0.06), radius: 10, y: 4) } .buttonStyle(.plain) } /// Favicon placeholder: first letter of the host in the container tint. private var siteBadge: some View { let letter = tab.page.url?.host()? .replacingOccurrences(of: "www.", with: "") .prefix(1).uppercased() return ZStack { Circle() .fill(container.color.color.opacity(0.16)) .frame(width: 30, height: 30) if let letter, !letter.isEmpty { Text(letter) .font(.footnote.bold()) .foregroundStyle(container.color.color) } else { Image(systemName: "globe") .font(.footnote.weight(.semibold)) .foregroundStyle(container.color.color) } } } }