import Observation import SwiftUI struct SybilSidebarView: View { @Bindable var viewModel: SybilViewModel private func iconName(for item: SidebarItem) -> String { switch item.kind { case .chat: return "message" case .search: return "globe" } } private func isSelected(_ item: SidebarItem) -> Bool { viewModel.draftKind == nil && viewModel.selectedItem == item.selection } var body: some View { VStack(spacing: 0) { VStack(alignment: .leading, spacing: 14) { SybilWordmark(size: 31) VStack(spacing: 10) { sidebarActionButton( title: "New chat", systemImage: "plus", isPrimary: true, isActive: viewModel.draftKind == .chat ) { viewModel.startNewChat() } sidebarActionButton( title: "New search", systemImage: "magnifyingglass", isPrimary: false, isActive: viewModel.draftKind == .search ) { viewModel.startNewSearch() } } } .padding(.horizontal, 12) .padding(.top, 18) .padding(.bottom, 10) Divider() .overlay(SybilTheme.border) if let errorMessage = viewModel.errorMessage { Text(errorMessage) .font(.sybil(.footnote)) .foregroundStyle(SybilTheme.danger) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) .padding(.vertical, 10) Divider() .overlay(SybilTheme.border) } if viewModel.isLoadingCollections && viewModel.sidebarItems.isEmpty { VStack(alignment: .leading, spacing: 8) { ProgressView() .tint(SybilTheme.primary) Text("Loading conversations…") .font(.sybil(.footnote)) .foregroundStyle(SybilTheme.textMuted) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(16) } else if viewModel.sidebarItems.isEmpty { VStack(spacing: 10) { Image(systemName: "message.badge") .font(.system(size: 20, weight: .medium)) .foregroundStyle(SybilTheme.textMuted) Text("Start a chat or run your first search.") .font(.sybil(.footnote)) .multilineTextAlignment(.center) .foregroundStyle(SybilTheme.textMuted) } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(16) } else { ScrollView { LazyVStack(alignment: .leading, spacing: 8) { ForEach(viewModel.sidebarItems) { item in Button { viewModel.select(item.selection) } label: { VStack(alignment: .leading, spacing: 6) { HStack(spacing: 8) { Image(systemName: iconName(for: item)) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(isSelected(item) ? SybilTheme.accent : SybilTheme.textMuted) .frame(width: 22, height: 22) .background( RoundedRectangle(cornerRadius: 7) .fill(isSelected(item) ? SybilTheme.accent.opacity(0.12) : SybilTheme.surface.opacity(0.72)) .overlay( RoundedRectangle(cornerRadius: 7) .stroke(isSelected(item) ? SybilTheme.accent.opacity(0.36) : SybilTheme.border.opacity(0.72), lineWidth: 1) ) ) Text(item.title) .font(.sybil(.subheadline, weight: .semibold)) .lineLimit(1) } HStack(spacing: 8) { Text(item.updatedAt.sybilRelativeLabel) .font(.sybil(.caption2)) .foregroundStyle(SybilTheme.textMuted) if let initiated = item.initiatedLabel { Spacer(minLength: 0) Text(initiated) .font(.sybil(.caption2)) .foregroundStyle(SybilTheme.textMuted.opacity(0.88)) .lineLimit(1) .multilineTextAlignment(.trailing) .frame(maxWidth: .infinity, alignment: .trailing) } } } .foregroundStyle(SybilTheme.text) .padding(.horizontal, 12) .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 12) .fill(isSelected(item) ? SybilTheme.selectedRowGradient : LinearGradient(colors: [SybilTheme.surface.opacity(0.56), SybilTheme.surface.opacity(0.36)], startPoint: .topLeading, endPoint: .bottomTrailing)) ) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(isSelected(item) ? SybilTheme.primary.opacity(0.55) : SybilTheme.border.opacity(0.72), lineWidth: 1) ) } .buttonStyle(.plain) .contextMenu { Button(role: .destructive) { Task { await viewModel.deleteItem(item.selection) } } label: { Label("Delete", systemImage: "trash") } } } } .padding(10) } } Divider() .overlay(SybilTheme.border) Button { viewModel.openSettings() } label: { Label("Settings", systemImage: "gearshape") .font(.sybil(.subheadline, weight: .medium)) .foregroundStyle(SybilTheme.text) .padding(.horizontal, 12) .padding(.vertical, 10) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 12) .fill(viewModel.selectedItem == .settings ? SybilTheme.primary.opacity(0.28) : Color.clear) ) } .buttonStyle(.plain) .padding(10) } .background(SybilTheme.panelGradient) } private func sidebarActionButton( title: String, systemImage: String, isPrimary: Bool, isActive: Bool, action: @escaping () -> Void ) -> some View { Button(action: action) { Label(title, systemImage: systemImage) .font(.sybil(.subheadline, weight: .semibold)) .foregroundStyle(isPrimary ? SybilTheme.text : SybilTheme.text.opacity(0.92)) .padding(.horizontal, 13) .padding(.vertical, 11) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 11) .fill(isPrimary ? SybilTheme.primaryGradient : LinearGradient(colors: [SybilTheme.surface.opacity(0.86), SybilTheme.surface.opacity(0.62)], startPoint: .topLeading, endPoint: .bottomTrailing)) ) .overlay( RoundedRectangle(cornerRadius: 11) .stroke(isActive ? SybilTheme.primary.opacity(0.70) : SybilTheme.border.opacity(isPrimary ? 0.28 : 0.72), lineWidth: 1) ) } .buttonStyle(.plain) } }