diff --git a/mac/Sources/Byre/Status.swift b/mac/Sources/Byre/Status.swift index cac691b..933300e 100644 --- a/mac/Sources/Byre/Status.swift +++ b/mac/Sources/Byre/Status.swift @@ -293,8 +293,34 @@ private struct RateLimits: View { } else { ViewThatFits(in: .horizontal) { grid - ScrollView(.horizontal) { - grid + stacked + } + } + } + + private var stacked: some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(limits.keys.sorted(), id: \.self) { vendor in + let usage = limits[vendor]! + vendorHeader(vendor, usage) + Divider().overlay(Palette.border) + ForEach(Array(usage.windows.enumerated()), id: \.offset) { _, w in + let used = usedFraction(w) + let elapsed = elapsedFraction(w) + VStack(alignment: .leading, spacing: 4) { + HStack(alignment: .firstTextBaseline, spacing: 10) { + Text(w.label) + Text(amountText(w)).foregroundStyle(Palette.muted) + Spacer(minLength: 0) + Text(usedText(w, used)).foregroundStyle(usedColor(used)) + } + PaceBar(used: used, elapsed: elapsed) + Text("Elapsed \(elapsed.map { percentText($0 * 100) } ?? "โ€”") ยท resets \(w.resetsAt.map(resetText) ?? "โ€”")") + .font(Fonts.font(11)) + .foregroundStyle(Palette.muted) + } + .padding(.vertical, 6) + Divider().overlay(Palette.border) } } } @@ -320,28 +346,40 @@ private struct RateLimits: View { Text(title).font(Fonts.font(11)).foregroundStyle(Palette.muted) } - @ViewBuilder - private func vendorRows(_ vendor: String, _ usage: VendorUsage) -> some View { + private func vendorHeader(_ vendor: String, _ usage: VendorUsage) -> some View { let rejected = usage.rejected != nil || usage.windows.contains { w in w.limit.map { w.used >= $0 } ?? false } let reason = usage.rejected.flatMap { $0.isEmpty ? nil : $0 } + return HStack(alignment: .firstTextBaseline, spacing: 10) { + Text(vendor).font(Fonts.font(13, weight: 700)).foregroundStyle(Palette.text) + Text(rejected ? "rejected\(reason.map { ": \($0)" } ?? "")" : "allowed").font(Fonts.font(11, weight: 700)).foregroundStyle(rejected ? Palette.danger : Palette.ok) + Text("as of \(since(usage.updatedAt))").font(Fonts.font(11)).foregroundStyle(Palette.muted) + } + .padding(.top, 12) + } + + private func usedFraction(_ w: UsageWindow) -> Double? { + w.limit.flatMap { $0 > 0 && (w.used / $0).isFinite ? w.used / $0 : nil } + } + + private func usedColor(_ used: Double?) -> Color { + used.map { $0 >= 0.9 ? Palette.danger : $0 >= 0.7 ? Palette.warn : Palette.text } ?? Palette.text + } + + @ViewBuilder + private func vendorRows(_ vendor: String, _ usage: VendorUsage) -> some View { GridRow { - HStack(alignment: .firstTextBaseline, spacing: 10) { - Text(vendor).font(Fonts.font(13, weight: 700)).foregroundStyle(Palette.text) - Text(rejected ? "rejected\(reason.map { ": \($0)" } ?? "")" : "allowed").font(Fonts.font(11, weight: 700)).foregroundStyle(rejected ? Palette.danger : Palette.ok) - Text("as of \(since(usage.updatedAt))").font(Fonts.font(11)).foregroundStyle(Palette.muted) - } - .padding(.top, 12) - .gridCellColumns(6) + vendorHeader(vendor, usage) + .gridCellColumns(6) } Divider().overlay(Palette.border) ForEach(Array(usage.windows.enumerated()), id: \.offset) { _, w in - let used = w.limit.flatMap { $0 > 0 && (w.used / $0).isFinite ? w.used / $0 : nil } + let used = usedFraction(w) let elapsed = elapsedFraction(w) GridRow { Text(w.label).fixedSize() Text(amountText(w)).fixedSize() Text(usedText(w, used)) - .foregroundStyle(used.map { $0 >= 0.9 ? Palette.danger : $0 >= 0.7 ? Palette.warn : Palette.text } ?? Palette.text) + .foregroundStyle(usedColor(used)) .fixedSize() Text(elapsed.map { percentText($0 * 100) } ?? "โ€”").fixedSize() PaceBar(used: used, elapsed: elapsed)