import AppKitimport CodexBarCoreimport SwiftUI /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels.struct UsageMenuCardView: View { struct Model { enum PercentStyle: String { case left case used var labelSuffix: String { switch self { case .left: L("usage_percent_suffix_left") case .used: L("usage_percent_suffix_used") } } var accessibilityLabel: String { switch self { case .left: L("Usage remaining") case .used: L("Usage used") } } } struct Metric: Identifiable { let id: String let title: String let percent: Double let percentStyle: PercentStyle let statusText: String? let resetText: String? let detailText: String? let detailLeftText: String? let detailRightText: String? let pacePercent: Double? let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] let cardStyle: Bool init( id: String, title: String, percent: Double, percentStyle: PercentStyle, statusText: String? = nil, resetText: String?, detailText: String?, detailLeftText: String?, detailRightText: String?, pacePercent: Double?, paceOnTop: Bool, warningMarkerPercents: [Double] = [], workdayMarkerPercents: [Double] = [], cardStyle: Bool = false) { self.id = id self.title = title self.percent = percent self.percentStyle = percentStyle self.statusText = statusText self.resetText = resetText self.detailText = detailText self.detailLeftText = detailLeftText self.detailRightText = detailRightText self.pacePercent = pacePercent self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents self.cardStyle = cardStyle } var percentLabel: String { UsageFormatter.percentText(self.percent, suffix: self.percentStyle.labelSuffix) } } enum SubtitleStyle { case info case loading case error } struct TokenUsageSection { let sessionLine: String let monthLine: String let comparisonLines: [String] let hintLine: String? let errorLine: String? let errorCopyText: String? init( sessionLine: String, monthLine: String, comparisonLines: [String] = [], hintLine: String?, errorLine: String?, errorCopyText: String?) { self.sessionLine = sessionLine self.monthLine = monthLine self.comparisonLines = comparisonLines self.hintLine = hintLine self.errorLine = errorLine self.errorCopyText = errorCopyText } } struct ProviderCostSection { let title: String let percentUsed: Double? let spendLine: String let percentLine: String? var personalSpendLine: String? } let provider: UsageProvider let providerName: String let email: String let subtitleText: String let subtitleStyle: SubtitleStyle var usesLiveSubtitle: Bool = false let planText: String? let metrics: [Metric] let usageNotes: [String] let openAIAPIUsage: OpenAIAPIUsageSnapshot? let inlineUsageDashboard: InlineUsageDashboardModel? let creditsText: String? let creditsRemaining: Double? var creditsProgressPercent: Double?, creditsScaleText: String? let creditsHintText: String? let creditsHintCopyText: String? var codexResetCredits: CodexResetCreditsPresentation? let providerCost: ProviderCostSection? let tokenUsage: TokenUsageSection? let placeholder: String? let progressColor: Color } let model: Model var layoutModel: Model? let width: CGFloat var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor static func popupMetricTitle(provider: UsageProvider, metric: Model.Metric) -> String { if provider == .openrouter, metric.id == "primary" { return L("API key limit") } return metric.title } var body: some View { let liveModel = self.liveModel VStack(alignment: .leading, spacing: 0) { UsageMenuCardHeaderView( model: self.layoutModel ?? self.model, planAction: self.planAction) if Self.hasDetails(for: liveModel) { Divider() .padding(.top, UsageMenuCardLayout.headerContentSpacing) .padding(.bottom, Self.dividerBottomPadding(for: liveModel)) } if !liveModel.usesStackedDetailLayout { if let dashboard = liveModel.inlineUsageDashboard { InlineUsageDashboardContent(model: dashboard) } else if !liveModel.usageNotes.isEmpty { UsageNotesContent(notes: liveModel.usageNotes) } else if let placeholder = liveModel.placeholder { // Non-stacked placeholders are standalone detail rows; stacked usage placeholders are gated below. Text(placeholder) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .font(.subheadline) } } else { let hasUsage = liveModel.hasUsageContent let hasCredits = liveModel.creditsText != nil let hasProviderCost = liveModel.providerCost != nil let hasCost = liveModel.tokenUsage != nil || hasProviderCost VStack(alignment: .leading, spacing: 12) { if hasUsage { UsageMenuCardUsageContentView(model: liveModel, showBottomDivider: false) } if hasUsage, hasCredits || hasCost { Divider() } if let credits = liveModel.creditsText { CreditsBarContent( creditsText: credits, creditsRemaining: liveModel.creditsRemaining, progressPercent: liveModel.creditsProgressPercent, scaleText: liveModel.creditsScaleText, hintText: liveModel.creditsHintText, hintCopyText: liveModel.creditsHintCopyText, progressColor: liveModel.progressColor) } if hasCredits, hasCost { Divider() } if let providerCost = liveModel.providerCost { ProviderCostContent( section: providerCost, progressColor: liveModel.progressColor) } if hasProviderCost, liveModel.tokenUsage != nil { Divider() } if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { Text(UsageMenuCardView.Model.tokenUsageHeader(provider: liveModel.provider)) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) .font(.footnote) .lineLimit(1) Text(tokenUsage.monthLine) .font(.footnote) .lineLimit(1) ForEach(tokenUsage.comparisonLines, id: \.self) { line in Text(line) .font(.footnote) .lineLimit(1) } if let hint = tokenUsage.hintLine, !hint.isEmpty { Text(hint) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } if let error = tokenUsage.errorLine, !error.isEmpty { Text(error) .font(.footnote) .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) .overlay { ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error) } } } } } } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding( .top, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionTopPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) // AppKit's following separator row adds visual bottom space, so detail cards keep this inset tight. .padding( .bottom, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionBottomPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) .frame(width: self.width, alignment: .leading) } private var liveModel: Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } private static func hasDetails(for model: Model) -> Bool { model.hasUsageContent || model.usesStackedDetailLayout } static func dividerBottomPadding(for model: Model) -> CGFloat { if model.usesStackedDetailLayout, model.hasUsageContent { return UsageMenuCardLayout.postHeaderDividerContentSpacing } return UsageMenuCardLayout.sectionBottomPadding }} private struct UsageMenuCardHeaderView: View { let model: UsageMenuCardView.Model var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { VStack(alignment: .leading, spacing: UsageMenuCardLayout.headerLineSpacing) { HStack(alignment: .firstTextBaseline, spacing: UsageMenuCardLayout.headerColumnSpacing) { Text(self.model.providerName).font(.headline) .fontWeight(.semibold) .lineLimit(1).truncationMode(.tail).layoutPriority(1) Spacer() Text(self.model.email).font(.subheadline) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1).truncationMode(.middle) } let liveSubtitle = self.liveSubtitle // Keep the geometry AppKit measured for this hosted row. A new error stays one line // until the next rebuild; a recovered error keeps its reserved height until then. let usesErrorLayout = self.model.subtitleStyle == .error let subtitleAlignment: VerticalAlignment = usesErrorLayout ? .top : .firstTextBaseline HStack(alignment: subtitleAlignment, spacing: UsageMenuCardLayout.headerColumnSpacing) { if usesErrorLayout { Text(self.model.subtitleText) .font(.footnote) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 4) .hidden() .overlay(alignment: .topLeading) { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) } .clipped() .layoutPriority(1) } else { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(1) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .layoutPriority(1) } Spacer() if usesErrorLayout { let showsCopyButton = liveSubtitle.style == .error && !liveSubtitle.text.isEmpty CopyIconButton( copyText: liveSubtitle.text, isHighlighted: self.isHighlighted, isInteractive: showsCopyButton) .opacity(showsCopyButton ? 1 : 0) .allowsHitTesting(showsCopyButton) .accessibilityHidden(!showsCopyButton) } if let plan = self.model.planText { Group { if let planAction { Button(action: planAction) { Text(plan) } .buttonStyle(.plain) .menuCardInteractiveControl() .accessibilityLabel(plan) } else { Text(plan) } } .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } } } private var liveSubtitle: MenuCardLiveSubtitle { let fallback = MenuCardLiveSubtitle(text: self.model.subtitleText, style: self.model.subtitleStyle) guard self.model.usesLiveSubtitle else { return fallback } return self.refreshMonitor?.subtitle(for: self.model.provider, fallback: fallback) ?? fallback } private func subtitleColor(for style: UsageMenuCardView.Model.SubtitleStyle) -> Color { switch style { case .info: MenuHighlightStyle.secondary(self.isHighlighted) case .loading: MenuHighlightStyle.secondary(self.isHighlighted) case .error: MenuHighlightStyle.error(self.isHighlighted) } }} private struct CopyIconButtonStyle: ButtonStyle { let isHighlighted: Bool func makeBody(configuration: Configuration) -> some View { configuration.label .padding(4) .background { RoundedRectangle(cornerRadius: 4, style: .continuous) .fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(configuration.isPressed ? 0.18 : 0)) } .scaleEffect(configuration.isPressed ? 0.94 : 1) .animation(.easeOut(duration: 0.12), value: configuration.isPressed) }} private struct CopyIconButton: View { let copyText: String let isHighlighted: Bool let isInteractive: Bool @State private var didCopy = false @State private var resetTask: Task<Void, Never>? var body: some View { Button { self.handleCopy() } label: { Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc") .font(.caption2.weight(.semibold)) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .frame(width: 18, height: 18) } .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted)) .menuCardInteractiveControl(isEnabled: self.isInteractive) .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy error")) } private func handleCopy() { let text = self.copyText self.resetTask?.cancel() MenuPasteboardCopy.perform(text, completion: { self.didCopy = true self.resetTask = Task { @MainActor in try? await Task.sleep(for: .seconds(0.9)) self.didCopy = false } }) }} private struct ProviderCostContent: View { let section: UsageMenuCardView.Model.ProviderCostSection let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.section.title) .font(.body) .fontWeight(.medium) if let percentUsed = self.section.percentUsed { UsageProgressBar( percent: percentUsed, tint: self.progressColor, accessibilityLabel: L("Extra usage spent")) } HStack(alignment: .firstTextBaseline) { Text(self.section.spendLine).font(.footnote).lineLimit(1) Spacer() if let percentLine = self.section.percentLine { Text(percentLine) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } if let personalSpendLine = self.section.personalSpendLine { Text(personalSpendLine) .font(.footnote).foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)).lineLimit(1) } } }} private struct MetricRow: View { let metric: UsageMenuCardView.Model.Metric let title: String let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.title) .font(.body) .fontWeight(.medium) if let statusText = self.metric.statusText { Text(statusText) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } else { UsageProgressBar( percent: self.metric.percent, tint: self.progressColor, accessibilityLabel: self.metric.percentStyle.accessibilityLabel, pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents, workdayMarkerPercents: self.metric.workdayMarkerPercents) VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline) { Text(self.metric.percentLabel) .font(.footnote) .lineLimit(1) Spacer() if let rightLabel = self.metric.resetText { Text(rightLabel) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1)Lines 1–500 of 1573. Load more
import AppKit import CodexBarCore import SwiftUI /// SwiftUI card used inside the NSMenu to mirror Apple's rich menu panels. struct UsageMenuCardView: View { struct Model { enum PercentStyle: String { case left case used var labelSuffix: String { switch self { case .left: L("usage_percent_suffix_left") case .used: L("usage_percent_suffix_used") } } var accessibilityLabel: String { switch self { case .left: L("Usage remaining") case .used: L("Usage used") } } } struct Metric: Identifiable { let id: String let title: String let percent: Double let percentStyle: PercentStyle let statusText: String? let resetText: String? let detailText: String? let detailLeftText: String? let detailRightText: String? let pacePercent: Double? let paceOnTop: Bool let warningMarkerPercents: [Double] let workdayMarkerPercents: [Double] let cardStyle: Bool init( id: String, title: String, percent: Double, percentStyle: PercentStyle, statusText: String? = nil, resetText: String?, detailText: String?, detailLeftText: String?, detailRightText: String?, pacePercent: Double?, paceOnTop: Bool, warningMarkerPercents: [Double] = [], workdayMarkerPercents: [Double] = [], cardStyle: Bool = false) { self.id = id self.title = title self.percent = percent self.percentStyle = percentStyle self.statusText = statusText self.resetText = resetText self.detailText = detailText self.detailLeftText = detailLeftText self.detailRightText = detailRightText self.pacePercent = pacePercent self.paceOnTop = paceOnTop self.warningMarkerPercents = warningMarkerPercents self.workdayMarkerPercents = workdayMarkerPercents self.cardStyle = cardStyle } var percentLabel: String { UsageFormatter.percentText(self.percent, suffix: self.percentStyle.labelSuffix) } } enum SubtitleStyle { case info case loading case error } struct TokenUsageSection { let sessionLine: String let monthLine: String let comparisonLines: [String] let hintLine: String? let errorLine: String? let errorCopyText: String? init( sessionLine: String, monthLine: String, comparisonLines: [String] = [], hintLine: String?, errorLine: String?, errorCopyText: String?) { self.sessionLine = sessionLine self.monthLine = monthLine self.comparisonLines = comparisonLines self.hintLine = hintLine self.errorLine = errorLine self.errorCopyText = errorCopyText } } struct ProviderCostSection { let title: String let percentUsed: Double? let spendLine: String let percentLine: String? var personalSpendLine: String? } let provider: UsageProvider let providerName: String let email: String let subtitleText: String let subtitleStyle: SubtitleStyle var usesLiveSubtitle: Bool = false let planText: String? let metrics: [Metric] let usageNotes: [String] let openAIAPIUsage: OpenAIAPIUsageSnapshot? let inlineUsageDashboard: InlineUsageDashboardModel? let creditsText: String? let creditsRemaining: Double? var creditsProgressPercent: Double?, creditsScaleText: String? let creditsHintText: String? let creditsHintCopyText: String? var codexResetCredits: CodexResetCreditsPresentation? let providerCost: ProviderCostSection? let tokenUsage: TokenUsageSection? let placeholder: String? let progressColor: Color } let model: Model var layoutModel: Model? let width: CGFloat var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor static func popupMetricTitle(provider: UsageProvider, metric: Model.Metric) -> String { if provider == .openrouter, metric.id == "primary" { return L("API key limit") } return metric.title } var body: some View { let liveModel = self.liveModel VStack(alignment: .leading, spacing: 0) { UsageMenuCardHeaderView( model: self.layoutModel ?? self.model, planAction: self.planAction) if Self.hasDetails(for: liveModel) { Divider() .padding(.top, UsageMenuCardLayout.headerContentSpacing) .padding(.bottom, Self.dividerBottomPadding(for: liveModel)) } if !liveModel.usesStackedDetailLayout { if let dashboard = liveModel.inlineUsageDashboard { InlineUsageDashboardContent(model: dashboard) } else if !liveModel.usageNotes.isEmpty { UsageNotesContent(notes: liveModel.usageNotes) } else if let placeholder = liveModel.placeholder { // Non-stacked placeholders are standalone detail rows; stacked usage placeholders are gated below. Text(placeholder) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .font(.subheadline) } } else { let hasUsage = liveModel.hasUsageContent let hasCredits = liveModel.creditsText != nil let hasProviderCost = liveModel.providerCost != nil let hasCost = liveModel.tokenUsage != nil || hasProviderCost VStack(alignment: .leading, spacing: 12) { if hasUsage { UsageMenuCardUsageContentView(model: liveModel, showBottomDivider: false) } if hasUsage, hasCredits || hasCost { Divider() } if let credits = liveModel.creditsText { CreditsBarContent( creditsText: credits, creditsRemaining: liveModel.creditsRemaining, progressPercent: liveModel.creditsProgressPercent, scaleText: liveModel.creditsScaleText, hintText: liveModel.creditsHintText, hintCopyText: liveModel.creditsHintCopyText, progressColor: liveModel.progressColor) } if hasCredits, hasCost { Divider() } if let providerCost = liveModel.providerCost { ProviderCostContent( section: providerCost, progressColor: liveModel.progressColor) } if hasProviderCost, liveModel.tokenUsage != nil { Divider() } if let tokenUsage = liveModel.tokenUsage { VStack(alignment: .leading, spacing: 6) { Text(UsageMenuCardView.Model.tokenUsageHeader(provider: liveModel.provider)) .font(.body) .fontWeight(.medium) Text(tokenUsage.sessionLine) .font(.footnote) .lineLimit(1) Text(tokenUsage.monthLine) .font(.footnote) .lineLimit(1) ForEach(tokenUsage.comparisonLines, id: \.self) { line in Text(line) .font(.footnote) .lineLimit(1) } if let hint = tokenUsage.hintLine, !hint.isEmpty { Text(hint) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) } if let error = tokenUsage.errorLine, !error.isEmpty { Text(error) .font(.footnote) .foregroundStyle(MenuHighlightStyle.error(self.isHighlighted)) .lineLimit(4) .fixedSize(horizontal: false, vertical: true) .overlay { ClickToCopyOverlay(copyText: tokenUsage.errorCopyText ?? error) } } } } } } } .padding(.horizontal, UsageMenuCardLayout.horizontalPadding) .padding( .top, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionTopPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) // AppKit's following separator row adds visual bottom space, so detail cards keep this inset tight. .padding( .bottom, Self.hasDetails(for: liveModel) ? UsageMenuCardLayout.sectionBottomPadding : UsageMenuCardLayout.headerOnlyVerticalPadding) .frame(width: self.width, alignment: .leading) } private var liveModel: Model { guard self.model.usesLiveSubtitle else { return self.model } return self.refreshMonitor?.model(for: self.model.provider, fallback: self.model) ?? self.model } private static func hasDetails(for model: Model) -> Bool { model.hasUsageContent || model.usesStackedDetailLayout } static func dividerBottomPadding(for model: Model) -> CGFloat { if model.usesStackedDetailLayout, model.hasUsageContent { return UsageMenuCardLayout.postHeaderDividerContentSpacing } return UsageMenuCardLayout.sectionBottomPadding } } private struct UsageMenuCardHeaderView: View { let model: UsageMenuCardView.Model var planAction: (() -> Void)? @Environment(\.menuItemHighlighted) private var isHighlighted @Environment(\.menuCardRefreshMonitor) private var refreshMonitor var body: some View { VStack(alignment: .leading, spacing: UsageMenuCardLayout.headerLineSpacing) { HStack(alignment: .firstTextBaseline, spacing: UsageMenuCardLayout.headerColumnSpacing) { Text(self.model.providerName).font(.headline) .fontWeight(.semibold) .lineLimit(1).truncationMode(.tail).layoutPriority(1) Spacer() Text(self.model.email).font(.subheadline) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1).truncationMode(.middle) } let liveSubtitle = self.liveSubtitle // Keep the geometry AppKit measured for this hosted row. A new error stays one line // until the next rebuild; a recovered error keeps its reserved height until then. let usesErrorLayout = self.model.subtitleStyle == .error let subtitleAlignment: VerticalAlignment = usesErrorLayout ? .top : .firstTextBaseline HStack(alignment: subtitleAlignment, spacing: UsageMenuCardLayout.headerColumnSpacing) { if usesErrorLayout { Text(self.model.subtitleText) .font(.footnote) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .padding(.bottom, 4) .hidden() .overlay(alignment: .topLeading) { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(4) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) } .clipped() .layoutPriority(1) } else { Text(liveSubtitle.text) .font(.footnote) .foregroundStyle(self.subtitleColor(for: liveSubtitle.style)) .lineLimit(1) .multilineTextAlignment(.leading) .fixedSize(horizontal: false, vertical: true) .layoutPriority(1) } Spacer() if usesErrorLayout { let showsCopyButton = liveSubtitle.style == .error && !liveSubtitle.text.isEmpty CopyIconButton( copyText: liveSubtitle.text, isHighlighted: self.isHighlighted, isInteractive: showsCopyButton) .opacity(showsCopyButton ? 1 : 0) .allowsHitTesting(showsCopyButton) .accessibilityHidden(!showsCopyButton) } if let plan = self.model.planText { Group { if let planAction { Button(action: planAction) { Text(plan) } .buttonStyle(.plain) .menuCardInteractiveControl() .accessibilityLabel(plan) } else { Text(plan) } } .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } } } private var liveSubtitle: MenuCardLiveSubtitle { let fallback = MenuCardLiveSubtitle(text: self.model.subtitleText, style: self.model.subtitleStyle) guard self.model.usesLiveSubtitle else { return fallback } return self.refreshMonitor?.subtitle(for: self.model.provider, fallback: fallback) ?? fallback } private func subtitleColor(for style: UsageMenuCardView.Model.SubtitleStyle) -> Color { switch style { case .info: MenuHighlightStyle.secondary(self.isHighlighted) case .loading: MenuHighlightStyle.secondary(self.isHighlighted) case .error: MenuHighlightStyle.error(self.isHighlighted) } } } private struct CopyIconButtonStyle: ButtonStyle { let isHighlighted: Bool func makeBody(configuration: Configuration) -> some View { configuration.label .padding(4) .background { RoundedRectangle(cornerRadius: 4, style: .continuous) .fill(MenuHighlightStyle.secondary(self.isHighlighted).opacity(configuration.isPressed ? 0.18 : 0)) } .scaleEffect(configuration.isPressed ? 0.94 : 1) .animation(.easeOut(duration: 0.12), value: configuration.isPressed) } } private struct CopyIconButton: View { let copyText: String let isHighlighted: Bool let isInteractive: Bool @State private var didCopy = false @State private var resetTask: Task<Void, Never>? var body: some View { Button { self.handleCopy() } label: { Image(systemName: self.didCopy ? "checkmark" : "doc.on.doc") .font(.caption2.weight(.semibold)) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .frame(width: 18, height: 18) } .buttonStyle(CopyIconButtonStyle(isHighlighted: self.isHighlighted)) .menuCardInteractiveControl(isEnabled: self.isInteractive) .accessibilityLabel(self.didCopy ? L("Copied") : L("Copy error")) } private func handleCopy() { let text = self.copyText self.resetTask?.cancel() MenuPasteboardCopy.perform(text, completion: { self.didCopy = true self.resetTask = Task { @MainActor in try? await Task.sleep(for: .seconds(0.9)) self.didCopy = false } }) } } private struct ProviderCostContent: View { let section: UsageMenuCardView.Model.ProviderCostSection let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.section.title) .font(.body) .fontWeight(.medium) if let percentUsed = self.section.percentUsed { UsageProgressBar( percent: percentUsed, tint: self.progressColor, accessibilityLabel: L("Extra usage spent")) } HStack(alignment: .firstTextBaseline) { Text(self.section.spendLine).font(.footnote).lineLimit(1) Spacer() if let percentLine = self.section.percentLine { Text(percentLine) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } } if let personalSpendLine = self.section.personalSpendLine { Text(personalSpendLine) .font(.footnote).foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)).lineLimit(1) } } } } private struct MetricRow: View { let metric: UsageMenuCardView.Model.Metric let title: String let progressColor: Color @Environment(\.menuItemHighlighted) private var isHighlighted var body: some View { VStack(alignment: .leading, spacing: 6) { Text(self.title) .font(.body) .fontWeight(.medium) if let statusText = self.metric.statusText { Text(statusText) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1) } else { UsageProgressBar( percent: self.metric.percent, tint: self.progressColor, accessibilityLabel: self.metric.percentStyle.accessibilityLabel, pacePercent: self.metric.pacePercent, paceOnTop: self.metric.paceOnTop, warningMarkerPercents: self.metric.warningMarkerPercents, workdayMarkerPercents: self.metric.workdayMarkerPercents) VStack(alignment: .leading, spacing: 2) { HStack(alignment: .firstTextBaseline) { Text(self.metric.percentLabel) .font(.footnote) .lineLimit(1) Spacer() if let rightLabel = self.metric.resetText { Text(rightLabel) .font(.footnote) .foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted)) .lineLimit(1)