import CodexBarCoreimport SwiftUIimport UniformTypeIdentifiers @MainActorstruct ProviderSidebarListView: View { let providers: [UsageProvider] let orderedProviders: [UsageProvider] @Bindable var store: UsageStore let isEnabled: (UsageProvider) -> Binding<Bool> let subtitle: (UsageProvider) -> String @Binding var searchText: String @Binding var selection: UsageProvider? let moveProviders: (IndexSet, Int) -> Void @State private var draggingProvider: UsageProvider? var body: some View { VStack(spacing: 8) { ProviderSidebarSearchField(searchText: self.$searchText) .padding(.horizontal, 8) .padding(.top, 8) ScrollView { VStack(spacing: 0) { if self.providers.isEmpty { Text(L("No matching providers")) .font(.caption) .foregroundStyle(.secondary) .frame(maxWidth: .infinity, minHeight: 80) } ForEach(self.providers, id: \.self) { provider in ProviderSidebarRowView( provider: provider, store: self.store, isEnabled: self.isEnabled(provider), subtitle: self.subtitle(provider), draggingProvider: self.$draggingProvider) .padding(.horizontal, 8) .background( RoundedRectangle(cornerRadius: 6, style: .continuous) .fill( self.selection == provider ? Color(nsColor: .selectedContentBackgroundColor) : Color.clear) .padding(.horizontal, 4)) .contentShape(Rectangle()) .onTapGesture { self.selection = provider } .onDrop( of: [UTType.plainText], delegate: ProviderSidebarDropDelegate( item: provider, providers: self.orderedProviders, dragging: self.$draggingProvider, moveProviders: self.moveProviders)) } } .padding(.vertical, 4) } } .background( RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous) .fill(Color(nsColor: .controlBackgroundColor).opacity(0.8))) .overlay( RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous) .stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1)) .clipShape(RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous)) .frame(minWidth: ProviderSettingsMetrics.sidebarWidth, maxWidth: ProviderSettingsMetrics.sidebarWidth) }} private struct ProviderSidebarSearchField: View { @Binding var searchText: String var body: some View { HStack(spacing: 6) { Image(systemName: "magnifyingglass") .foregroundStyle(.secondary) .accessibilityHidden(true) TextField(L("Search providers"), text: self.$searchText) .textFieldStyle(.plain) if !self.searchText.isEmpty { Button { self.searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(.secondary) .accessibilityLabel(L("Clear")) } .buttonStyle(.plain) } } .font(.callout) .padding(.horizontal, 8) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 6, style: .continuous) .fill(Color(nsColor: .textBackgroundColor))) .overlay( RoundedRectangle(cornerRadius: 6, style: .continuous) .stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1)) }} @MainActorprivate struct ProviderSidebarRowView: View { let provider: UsageProvider @Bindable var store: UsageStore @Binding var isEnabled: Bool let subtitle: String @Binding var draggingProvider: UsageProvider? var body: some View { let isRefreshing = self.store.refreshingProviders.contains(self.provider) let showStatus = self.store.statusChecksEnabled let statusText = self.statusText HStack(alignment: .center, spacing: 10) { ProviderSidebarReorderHandle() .contentShape(Rectangle()) .padding(.vertical, 4) .padding(.horizontal, 2) .help(L("Drag to reorder")) .onDrag { self.draggingProvider = self.provider return NSItemProvider(object: self.provider.rawValue as NSString) } ProviderSidebarBrandIcon(provider: self.provider) VStack(alignment: .leading, spacing: 2) { HStack(spacing: 6) { Text(self.store.metadata(for: self.provider).displayName) .font(.subheadline.weight(.semibold)) .foregroundStyle(.primary) if showStatus { ProviderStatusDot(indicator: self.store.statusIndicator(for: self.provider)) } if isRefreshing { ProgressView() .controlSize(.mini) } } Text(statusText) .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) .frame(height: ProviderSettingsMetrics.sidebarSubtitleHeight, alignment: .topLeading) } Spacer(minLength: 8) Toggle("", isOn: self.$isEnabled) .labelsHidden() .toggleStyle(.checkbox) .controlSize(.small) } .padding(.trailing, 6) .contentShape(Rectangle()) .padding(.vertical, 2) } private var statusText: String { guard !self.isEnabled else { return self.subtitle } let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false) if lines.count >= 2 { let first = lines[0] let rest = lines.dropFirst().joined(separator: "\n") return "\(L("Disabled")) — \(first)\n\(rest)" } return "\(L("Disabled")) — \(self.subtitle)" }} private struct ProviderSidebarReorderHandle: View { var body: some View { VStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) { ForEach(0..<3, id: \.self) { _ in HStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) { Circle() .frame( width: ProviderSettingsMetrics.reorderDotSize, height: ProviderSettingsMetrics.reorderDotSize) Circle() .frame( width: ProviderSettingsMetrics.reorderDotSize, height: ProviderSettingsMetrics.reorderDotSize) } } } .frame( width: ProviderSettingsMetrics.reorderHandleSize, height: ProviderSettingsMetrics.reorderHandleSize) .foregroundStyle(.tertiary) .accessibilityLabel(L("Reorder")) }} @MainActorprivate struct ProviderSidebarBrandIcon: View { let provider: UsageProvider var body: some View { if let brand = ProviderBrandIcon.image(for: self.provider) { Image(nsImage: brand) .resizable() .scaledToFit() .frame(width: ProviderSettingsMetrics.iconSize, height: ProviderSettingsMetrics.iconSize) .foregroundStyle(.secondary) .accessibilityHidden(true) } else { Image(systemName: "circle.dotted") .font(.system(size: ProviderSettingsMetrics.iconSize, weight: .regular)) .foregroundStyle(.secondary) .accessibilityHidden(true) } }} private struct ProviderSidebarDropDelegate: DropDelegate { let item: UsageProvider let providers: [UsageProvider] @Binding var dragging: UsageProvider? let moveProviders: (IndexSet, Int) -> Void func dropEntered(info _: DropInfo) { guard let dragging, dragging != self.item else { return } guard let fromIndex = self.providers.firstIndex(of: dragging), let toIndex = self.providers.firstIndex(of: self.item) else { return } if fromIndex == toIndex { return } let adjustedIndex = toIndex > fromIndex ? toIndex + 1 : toIndex self.moveProviders(IndexSet(integer: fromIndex), adjustedIndex) } func dropUpdated(info _: DropInfo) -> DropProposal? { DropProposal(operation: .move) } func performDrop(info _: DropInfo) -> Bool { self.dragging = nil return true }} private struct ProviderStatusDot: View { let indicator: ProviderStatusIndicator var body: some View { Circle() .fill(self.statusColor) .frame(width: 6, height: 6) .accessibilityHidden(true) } private var statusColor: Color { switch self.indicator { case .none: .green case .minor: .yellow case .major: .orange case .critical: .red case .maintenance: .gray case .unknown: .gray } }}
import CodexBarCore
import SwiftUI
import UniformTypeIdentifiers
@MainActor
struct ProviderSidebarListView: View {
let providers: [UsageProvider]
let orderedProviders: [UsageProvider]
@Bindable var store: UsageStore
let isEnabled: (UsageProvider) -> Binding<Bool>
let subtitle: (UsageProvider) -> String
@Binding var searchText: String
@Binding var selection: UsageProvider?
let moveProviders: (IndexSet, Int) -> Void
@State private var draggingProvider: UsageProvider?
var body: some View {
VStack(spacing: 8) {
ProviderSidebarSearchField(searchText: self.$searchText)
.padding(.horizontal, 8)
.padding(.top, 8)
ScrollView {
VStack(spacing: 0) {
if self.providers.isEmpty {
Text(L("No matching providers"))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, minHeight: 80)
}
ForEach(self.providers, id: \.self) { provider in
ProviderSidebarRowView(
provider: provider,
store: self.store,
isEnabled: self.isEnabled(provider),
subtitle: self.subtitle(provider),
draggingProvider: self.$draggingProvider)
.padding(.horizontal, 8)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(
self.selection == provider
? Color(nsColor: .selectedContentBackgroundColor)
: Color.clear)
.padding(.horizontal, 4))
.contentShape(Rectangle())
.onTapGesture { self.selection = provider }
.onDrop(
of: [UTType.plainText],
delegate: ProviderSidebarDropDelegate(
item: provider,
providers: self.orderedProviders,
dragging: self.$draggingProvider,
moveProviders: self.moveProviders))
}
}
.padding(.vertical, 4)
}
}
.background(
RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous)
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.8)))
.overlay(
RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous)
.stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1))
.clipShape(RoundedRectangle(cornerRadius: ProviderSettingsMetrics.sidebarCornerRadius, style: .continuous))
.frame(minWidth: ProviderSettingsMetrics.sidebarWidth, maxWidth: ProviderSettingsMetrics.sidebarWidth)
}
}
private struct ProviderSidebarSearchField: View {
@Binding var searchText: String
var body: some View {
HStack(spacing: 6) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
.accessibilityHidden(true)
TextField(L("Search providers"), text: self.$searchText)
.textFieldStyle(.plain)
if !self.searchText.isEmpty {
Button {
self.searchText = ""
} label: {
Image(systemName: "xmark.circle.fill")
.foregroundStyle(.secondary)
.accessibilityLabel(L("Clear"))
}
.buttonStyle(.plain)
}
}
.font(.callout)
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.fill(Color(nsColor: .textBackgroundColor)))
.overlay(
RoundedRectangle(cornerRadius: 6, style: .continuous)
.stroke(Color(nsColor: .separatorColor).opacity(0.7), lineWidth: 1))
}
}
@MainActor
private struct ProviderSidebarRowView: View {
let provider: UsageProvider
@Bindable var store: UsageStore
@Binding var isEnabled: Bool
let subtitle: String
@Binding var draggingProvider: UsageProvider?
var body: some View {
let isRefreshing = self.store.refreshingProviders.contains(self.provider)
let showStatus = self.store.statusChecksEnabled
let statusText = self.statusText
HStack(alignment: .center, spacing: 10) {
ProviderSidebarReorderHandle()
.contentShape(Rectangle())
.padding(.vertical, 4)
.padding(.horizontal, 2)
.help(L("Drag to reorder"))
.onDrag {
self.draggingProvider = self.provider
return NSItemProvider(object: self.provider.rawValue as NSString)
}
ProviderSidebarBrandIcon(provider: self.provider)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(self.store.metadata(for: self.provider).displayName)
.font(.subheadline.weight(.semibold))
.foregroundStyle(.primary)
if showStatus {
ProviderStatusDot(indicator: self.store.statusIndicator(for: self.provider))
}
if isRefreshing {
ProgressView()
.controlSize(.mini)
}
}
Text(statusText)
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(2)
.frame(height: ProviderSettingsMetrics.sidebarSubtitleHeight, alignment: .topLeading)
}
Spacer(minLength: 8)
Toggle("", isOn: self.$isEnabled)
.labelsHidden()
.toggleStyle(.checkbox)
.controlSize(.small)
}
.padding(.trailing, 6)
.contentShape(Rectangle())
.padding(.vertical, 2)
}
private var statusText: String {
guard !self.isEnabled else { return self.subtitle }
let lines = self.subtitle.split(separator: "\n", omittingEmptySubsequences: false)
if lines.count >= 2 {
let first = lines[0]
let rest = lines.dropFirst().joined(separator: "\n")
return "\(L("Disabled")) — \(first)\n\(rest)"
}
return "\(L("Disabled")) — \(self.subtitle)"
}
}
private struct ProviderSidebarReorderHandle: View {
var body: some View {
VStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) {
ForEach(0..<3, id: \.self) { _ in
HStack(spacing: ProviderSettingsMetrics.reorderDotSpacing) {
Circle()
.frame(
width: ProviderSettingsMetrics.reorderDotSize,
height: ProviderSettingsMetrics.reorderDotSize)
Circle()
.frame(
width: ProviderSettingsMetrics.reorderDotSize,
height: ProviderSettingsMetrics.reorderDotSize)
}
}
}
.frame(
width: ProviderSettingsMetrics.reorderHandleSize,
height: ProviderSettingsMetrics.reorderHandleSize)
.foregroundStyle(.tertiary)
.accessibilityLabel(L("Reorder"))
}
}
@MainActor
private struct ProviderSidebarBrandIcon: View {
let provider: UsageProvider
var body: some View {
if let brand = ProviderBrandIcon.image(for: self.provider) {
Image(nsImage: brand)
.resizable()
.scaledToFit()
.frame(width: ProviderSettingsMetrics.iconSize, height: ProviderSettingsMetrics.iconSize)
.foregroundStyle(.secondary)
.accessibilityHidden(true)
} else {
Image(systemName: "circle.dotted")
.font(.system(size: ProviderSettingsMetrics.iconSize, weight: .regular))
.foregroundStyle(.secondary)
.accessibilityHidden(true)
}
}
}
private struct ProviderSidebarDropDelegate: DropDelegate {
let item: UsageProvider
let providers: [UsageProvider]
@Binding var dragging: UsageProvider?
let moveProviders: (IndexSet, Int) -> Void
func dropEntered(info _: DropInfo) {
guard let dragging, dragging != self.item else { return }
guard let fromIndex = self.providers.firstIndex(of: dragging),
let toIndex = self.providers.firstIndex(of: self.item)
else { return }
if fromIndex == toIndex { return }
let adjustedIndex = toIndex > fromIndex ? toIndex + 1 : toIndex
self.moveProviders(IndexSet(integer: fromIndex), adjustedIndex)
}
func dropUpdated(info _: DropInfo) -> DropProposal? {
DropProposal(operation: .move)
}
func performDrop(info _: DropInfo) -> Bool {
self.dragging = nil
return true
}
}
private struct ProviderStatusDot: View {
let indicator: ProviderStatusIndicator
var body: some View {
Circle()
.fill(self.statusColor)
.frame(width: 6, height: 6)
.accessibilityHidden(true)
}
private var statusColor: Color {
switch self.indicator {
case .none: .green
case .minor: .yellow
case .major: .orange
case .critical: .red
case .maintenance: .gray
case .unknown: .gray
}
}
}