Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions Sources/CodexBar/MenuCardHeightFingerprint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ extension UsageMenuCardView.Model.Metric {
MenuCardHeightFingerprint.field("detail", self.detailText),
MenuCardHeightFingerprint.field("detailLeft", self.detailLeftText),
MenuCardHeightFingerprint.field("detailRight", self.detailRightText),
self.detailRightSecondaryText == nil ? "detailRightSecondary=0" : "detailRightSecondary=1",
MenuCardHeightFingerprint.field(
"sessionEquivalentVerdict",
self.sessionEquivalentDetail?.verdictText),
Expand Down Expand Up @@ -129,10 +130,7 @@ extension UsageMenuCardView.Model.TokenUsageSection {

extension CodexResetCreditsPresentation {
fileprivate var heightFingerprint: String {
MenuCardHeightFingerprint.join([
MenuCardHeightFingerprint.field("text", self.text),
MenuCardHeightFingerprint.field("expirySummary", self.expirySummaryText),
])
"items=\(self.items.count)"
}
}

Expand Down
54 changes: 39 additions & 15 deletions Sources/CodexBar/MenuCardView+CodexResetCredits.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,8 @@ struct CodexResetCreditsPresentation: Equatable {
let text: String
let items: [CodexResetCreditPresentationItem]

var expirySummaryText: String {
let visibleItems = self.items.prefix(4).map(\.compactExpiryText)
let hiddenCount = self.items.count - visibleItems.count
let suffix = hiddenCount > 0 ? ["+\(hiddenCount)"] : []
return (visibleItems + suffix).joined(separator: " · ")
var compactExpiryTexts: [String] {
self.items.map(\.compactExpiryText)
}

var helpText: String {
Expand Down Expand Up @@ -57,14 +54,37 @@ struct CodexResetCreditsPresentation: Equatable {
return CodexResetCreditPresentationItem(expiryText: L("No expiry"), compactExpiryText: L("No expiry"))
}
let formattedTime = Self.formattedTime(expiresAt, resetStyle: resetStyle, now: now)
let compactExpiryText = resetStyle == .countdown && formattedTime.hasPrefix("in ")
? String(formattedTime.dropFirst(3))
: formattedTime
let compactExpiryText = Self.compactExpiryText(
expiresAt,
resetStyle: resetStyle,
formattedTime: formattedTime)
return CodexResetCreditPresentationItem(
expiryText: String(format: L("Expires %@"), formattedTime),
compactExpiryText: compactExpiryText)
}

private static func compactExpiryText(
_ expiresAt: Date,
resetStyle: ResetTimeDisplayStyle,
formattedTime: String) -> String
{
switch resetStyle {
case .absolute:
self.fixedCompactDateText(expiresAt)
case .countdown:
formattedTime.hasPrefix("in ") ? String(formattedTime.dropFirst(3)) : formattedTime
}
}

private static func fixedCompactDateText(_ date: Date) -> String {
let components = Calendar.current.dateComponents([.month, .day, .hour, .minute], from: date)
let month = components.month ?? 0
let day = components.day ?? 0
let hour = components.hour ?? 0
let minute = components.minute ?? 0
return String(format: "%02d/%02d %02d:%02d", month, day, hour, minute)
}

private static func formattedTime(
_ expiresAt: Date,
resetStyle: ResetTimeDisplayStyle,
Expand All @@ -90,21 +110,25 @@ struct CodexResetCreditsContent: View {
.font(.body)
.fontWeight(.medium)
.lineLimit(1)
HStack(alignment: .firstTextBaseline, spacing: 8) {
HStack(alignment: .top, spacing: 8) {
Text(self.presentation.text)
.font(.footnote.weight(.semibold))
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
.layoutPriority(1)
Spacer(minLength: 8)
HStack(alignment: .firstTextBaseline, spacing: 4) {
HStack(alignment: .top, spacing: 4) {
Image(systemName: "clock")
.font(.caption2)
Text(self.presentation.expirySummaryText)
.font(.caption)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
.minimumScaleFactor(0.8)
VStack(alignment: .leading, spacing: 2) {
ForEach(self.presentation.items.indices, id: \.self) { index in
Text(self.presentation.items[index].compactExpiryText)
.font(.caption)
.monospacedDigit()
.lineLimit(1)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.accessibilityHidden(true)
Expand Down
22 changes: 21 additions & 1 deletion Sources/CodexBar/MenuCardView+ModelHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ extension UsageMenuCardView.Model {
struct PaceDetail {
let leftLabel: String
let rightLabel: String?
var riskLabel: String?
let pacePercent: Double?
let paceOnTop: Bool
}
Expand Down Expand Up @@ -41,6 +42,9 @@ extension UsageMenuCardView.Model {
metricID: metric.id),
detailLeftText: PersonalInfoRedactor.redactEmails(in: metric.detailLeftText, isEnabled: true),
detailRightText: PersonalInfoRedactor.redactEmails(in: metric.detailRightText, isEnabled: true),
detailRightSecondaryText: PersonalInfoRedactor.redactEmails(
in: metric.detailRightSecondaryText,
isEnabled: true),
pacePercent: metric.pacePercent,
paceOnTop: metric.paceOnTop,
warningMarkerPercents: metric.warningMarkerPercents,
Expand Down Expand Up @@ -174,17 +178,29 @@ extension UsageMenuCardView.Model {
}

private static func hasCompatibleMetricLayout(_ current: Metric, _ candidate: Metric) -> Bool {
current.id == candidate.id &&
// Numeric substitutions retain the same text shape; wording changes can cross the one-row/two-row boundary.
let hasCompatibleAdaptiveDetailText = current.detailRightSecondaryText != nil ||
(Self.adaptiveDetailTextShape(current.detailLeftText) ==
Self.adaptiveDetailTextShape(candidate.detailLeftText) &&
Self.adaptiveDetailTextShape(current.detailRightText) ==
Self.adaptiveDetailTextShape(candidate.detailRightText))
return current.id == candidate.id &&
current.title == candidate.title &&
current.percentStyle == candidate.percentStyle &&
(current.statusText == nil) == (candidate.statusText == nil) &&
(current.resetText == nil) == (candidate.resetText == nil) &&
(current.detailText == nil) == (candidate.detailText == nil) &&
(current.detailLeftText == nil) == (candidate.detailLeftText == nil) &&
(current.detailRightText == nil) == (candidate.detailRightText == nil) &&
(current.detailRightSecondaryText == nil) == (candidate.detailRightSecondaryText == nil) &&
Comment on lines 193 to +195

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Re-measure when metric detail text changes rows

With the new MetricDetailRow, rows without detailRightSecondaryText can change height based on the actual detail strings because ViewThatFits falls back from one row to two rows when the text no longer fits. This compatibility check still only compares whether the detail fields are nil, so an open live card can accept a refresh that changes detailLeftText/detailRightText from short to long as the same layout; the existing NSMenuItem height is then not rebuilt/re-measured and the new second row can be clipped until the menu is reopened. Reject compatibility or compare enough text shape to cover the one-row/two-row transition.

Useful? React with 👍 / 👎.

hasCompatibleAdaptiveDetailText &&
current.cardStyle == candidate.cardStyle
}

private static func adaptiveDetailTextShape(_ text: String?) -> String? {
text.map { String($0.map { character in character.isNumber ? "#" : character }) }
}

private static func hasCompatibleCreditsLayout(
currentText: String?,
currentRemaining: Double?,
Expand Down Expand Up @@ -464,6 +480,7 @@ extension UsageMenuCardView.Model {
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
riskLabel: detail.riskLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
Expand Down Expand Up @@ -493,6 +510,7 @@ extension UsageMenuCardView.Model {
return PaceDetail(
leftLabel: detail.leftLabel,
rightLabel: detail.rightLabel,
riskLabel: detail.riskLabel,
pacePercent: pacePercent,
paceOnTop: paceOnTop)
}
Expand Down Expand Up @@ -695,6 +713,7 @@ extension UsageMenuCardView.Model {
detailText: usageKnown ? detailText : nil,
detailLeftText: usageKnown ? paceDetail?.leftLabel : nil,
detailRightText: usageKnown ? paceDetail?.rightLabel : nil,
detailRightSecondaryText: usageKnown ? paceDetail?.riskLabel : nil,
pacePercent: usageKnown ? paceDetail?.pacePercent : nil,
paceOnTop: paceDetail?.paceOnTop ?? true,
sessionEquivalentDetail: usageKnown
Expand Down Expand Up @@ -852,6 +871,7 @@ extension UsageMenuCardView.Model {
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
detailRightSecondaryText: paceDetail?.riskLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true)
}
Expand Down
1 change: 1 addition & 0 deletions Sources/CodexBar/MenuCardView+SessionEquivalent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ extension UsageMenuCardView.Model {
detailText: nil,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
detailRightSecondaryText: paceDetail?.riskLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
Expand Down
35 changes: 19 additions & 16 deletions Sources/CodexBar/MenuCardView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ struct UsageMenuCardView: View {
let detailText: String?
let detailLeftText: String?
let detailRightText: String?
let detailRightSecondaryText: String?
let pacePercent: Double?
let paceOnTop: Bool
let warningMarkerPercents: [Double]
Expand All @@ -51,6 +52,7 @@ struct UsageMenuCardView: View {
detailText: String?,
detailLeftText: String?,
detailRightText: String?,
detailRightSecondaryText: String? = nil,
pacePercent: Double?,
paceOnTop: Bool,
warningMarkerPercents: [Double] = [],
Expand All @@ -67,6 +69,7 @@ struct UsageMenuCardView: View {
self.detailText = detailText
self.detailLeftText = detailLeftText
self.detailRightText = detailRightText
self.detailRightSecondaryText = detailRightSecondaryText
self.pacePercent = pacePercent
self.paceOnTop = paceOnTop
self.warningMarkerPercents = warningMarkerPercents
Expand Down Expand Up @@ -534,22 +537,14 @@ private struct MetricRow: View {
.lineLimit(1)
}
}
if self.metric.detailLeftText != nil || self.metric.detailRightText != nil {
HStack(alignment: .firstTextBaseline) {
if let detailLeft = self.metric.detailLeftText {
Text(detailLeft)
.font(.footnote)
.foregroundStyle(MenuHighlightStyle.primary(self.isHighlighted))
.lineLimit(1)
}
Spacer()
if let detailRight = self.metric.detailRightText {
Text(detailRight)
.font(.footnote)
.foregroundStyle(MenuHighlightStyle.secondary(self.isHighlighted))
.lineLimit(1)
}
}
if self.metric.detailLeftText != nil ||
self.metric.detailRightText != nil ||
self.metric.detailRightSecondaryText != nil
{
MetricDetailRow(
leftText: self.metric.detailLeftText,
rightText: self.metric.detailRightText,
secondaryRightText: self.metric.detailRightSecondaryText)
}
if let sessionEquivalentDetail = self.metric.sessionEquivalentDetail {
Text(sessionEquivalentDetail.verdictText)
Expand Down Expand Up @@ -1260,6 +1255,7 @@ extension UsageMenuCardView.Model {
detailText: tertiaryDetailText,
detailLeftText: tertiaryPaceDetail?.leftLabel,
detailRightText: tertiaryPaceDetail?.rightLabel,
detailRightSecondaryText: tertiaryPaceDetail?.riskLabel,
pacePercent: tertiaryPaceDetail?.pacePercent,
paceOnTop: tertiaryPaceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
Expand Down Expand Up @@ -1318,6 +1314,7 @@ extension UsageMenuCardView.Model {
var primaryResetText = Self.resetText(for: primary, style: input.resetTimeDisplayStyle, now: input.now)
var primaryDetailLeft: String?
var primaryDetailRight: String?
var primaryDetailRightSecondary: String?
if input.provider == .crof,
let detail = primary.resetDescription?.trimmingCharacters(in: .whitespacesAndNewlines),
!detail.isEmpty
Expand Down Expand Up @@ -1382,6 +1379,7 @@ extension UsageMenuCardView.Model {
{
primaryDetailLeft = paceDetail.leftLabel
primaryDetailRight = paceDetail.rightLabel
primaryDetailRightSecondary = paceDetail.riskLabel
primaryPacePercent = paceDetail.pacePercent
primaryPaceOnTop = paceDetail.paceOnTop
}
Expand All @@ -1404,13 +1402,15 @@ extension UsageMenuCardView.Model {
if let paceDetail {
primaryDetailLeft = paceDetail.leftLabel
primaryDetailRight = paceDetail.rightLabel
primaryDetailRightSecondary = paceDetail.riskLabel
primaryPacePercent = paceDetail.pacePercent
primaryPaceOnTop = paceDetail.paceOnTop
}
}
} else if let paceDetail = Self.resetWindowPaceDetail(window: primary, input: input) {
primaryDetailLeft = paceDetail.leftLabel
primaryDetailRight = paceDetail.rightLabel
primaryDetailRightSecondary = paceDetail.riskLabel
primaryPacePercent = paceDetail.pacePercent
primaryPaceOnTop = paceDetail.paceOnTop
}
Expand All @@ -1431,6 +1431,7 @@ extension UsageMenuCardView.Model {
primaryResetText = regen.resetText
primaryDetailLeft = regen.pace.leftLabel
primaryDetailRight = regen.pace.rightLabel
primaryDetailRightSecondary = regen.pace.riskLabel
primaryPacePercent = regen.pace.pacePercent
primaryPaceOnTop = regen.pace.paceOnTop
}
Expand All @@ -1450,6 +1451,7 @@ extension UsageMenuCardView.Model {
detailText: primaryDetailText,
detailLeftText: primaryDetailLeft,
detailRightText: primaryDetailRight,
detailRightSecondaryText: primaryDetailRightSecondary,
pacePercent: primaryPacePercent,
paceOnTop: primaryPaceOnTop,
warningMarkerPercents: Self.warningMarkerPercents(
Expand Down Expand Up @@ -1569,6 +1571,7 @@ extension UsageMenuCardView.Model {
detailText: weeklyDetailText,
detailLeftText: paceDetail?.leftLabel,
detailRightText: paceDetail?.rightLabel,
detailRightSecondaryText: paceDetail?.riskLabel,
pacePercent: paceDetail?.pacePercent,
paceOnTop: paceDetail?.paceOnTop ?? true,
warningMarkerPercents: Self.warningMarkerPercents(
Expand Down
28 changes: 22 additions & 6 deletions Sources/CodexBar/PreferencesProviderDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -507,20 +507,31 @@ private struct ProviderMetricInlineRow: View {
Text(leftDetail)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
Spacer(minLength: 8)
if let rightDetail = self.metric.detailRightText, !rightDetail.isEmpty {
Text(rightDetail)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
} else if !resetText.isEmpty {
Text(resetText)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
}
}
}

if let secondaryRightDetail = self.metric.detailRightSecondaryText, !secondaryRightDetail.isEmpty {
Text(secondaryRightDetail)
.font(.footnote)
.foregroundStyle(.secondary)
.lineLimit(1)
.frame(maxWidth: .infinity, alignment: .trailing)
}

if hasRightDetail, !resetText.isEmpty {
Text(resetText)
.font(.footnote)
Expand Down Expand Up @@ -552,19 +563,24 @@ private struct ProviderCodexResetCreditsInlineRow: View {
.font(.footnote)
.foregroundStyle(.secondary)
}
HStack(alignment: .firstTextBaseline, spacing: 4) {
HStack(alignment: .top, spacing: 4) {
Image(systemName: "clock")
.font(.caption2)
Text(self.presentation.expirySummaryText)
.font(.caption)
.foregroundStyle(.tertiary)
.lineLimit(1)
.minimumScaleFactor(0.8)
VStack(alignment: .leading, spacing: 2) {
ForEach(self.presentation.items.indices, id: \.self) { index in
Text(self.presentation.items[index].compactExpiryText)
.font(.caption)
.foregroundStyle(.tertiary)
.monospacedDigit()
.lineLimit(1)
}
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
.accessibilityHidden(true)
}
.padding(.vertical, 2)
.help(self.presentation.helpText)
.accessibilityElement(children: .combine)
.accessibilityLabel(self.presentation.accessibilityLabel)
}
Expand Down
Loading
Loading