import CodexBarCoreimport SwiftUIimport UniformTypeIdentifiers @MainActorstruct ProviderSidebarListView: View { let providers: [UsageProvider] @Bindable var store: UsageStore let isEnabled: (UsageProvider) -> Binding<Bool> let subtitle: (UsageProvider) -> String @Binding var selection: UsageProvider? let moveProviders: (IndexSet, Int) -> Void @State private var draggingProvider: UsageProvider? var body: some View { List(selection: self.$selection) { ForEach(self.providers, id: \.self) { provider in ProviderSidebarRowView( provider: provider, store: self.store, isEnabled: self.isEnabled(provider), subtitle: self.subtitle(provider), draggingProvider: self.$draggingProvider) .tag(provider) .onDrop( of: [UTType.plainText], delegate: ProviderSidebarDropDelegate( item: provider, providers: self.providers, dragging: self.$draggingProvider, moveProviders: self.moveProviders)) } } .listStyle(.sidebar) .scrollContentBackground(.hidden) .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) }} @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("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) } .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 "Disabled — \(first)\n\(rest)" } return "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("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]
@Bindable var store: UsageStore
let isEnabled: (UsageProvider) -> Binding<Bool>
let subtitle: (UsageProvider) -> String
@Binding var selection: UsageProvider?
let moveProviders: (IndexSet, Int) -> Void
@State private var draggingProvider: UsageProvider?
var body: some View {
List(selection: self.$selection) {
ForEach(self.providers, id: \.self) { provider in
ProviderSidebarRowView(
provider: provider,
store: self.store,
isEnabled: self.isEnabled(provider),
subtitle: self.subtitle(provider),
draggingProvider: self.$draggingProvider)
.tag(provider)
.onDrop(
of: [UTType.plainText],
delegate: ProviderSidebarDropDelegate(
item: provider,
providers: self.providers,
dragging: self.$draggingProvider,
moveProviders: self.moveProviders))
}
}
.listStyle(.sidebar)
.scrollContentBackground(.hidden)
.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)
}
}
@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("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)
}
.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 "Disabled — \(first)\n\(rest)"
}
return "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("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
}
}
}