Unify capsule, station label and price rating styling
One adaptive tone palette and one shape rule now drive every capsule on Stations, Favourites, the detail sheet and the Key. - CapsuleTone: the light/dark palette pairs in one place, resolved through a UIColor dynamic provider so a List row that re-renders without rebuilding its body still follows an appearance switch. Every pair clears 4.5:1 foreground-on-background in both appearances (4.96-6.72:1). - AdaptiveCapsule: capsule at one drawn line, rounded rect at two or more, radius 8 -> 10, measuring content before padding so insets cannot inflate the line count. - Listing verdict capsules move from white-on-muted-solid to the tinted palette, so Saves, Break even, Costs and Closest station read the same in the rows and in the detail sheet. - Best Value is purple, not green: green stays reserved for a financial saving, which is what the Saves capsule beside it means. - Price dots standardise to 12pt in the app (widget keeps its own 6pt dot; PriceRatingDot now takes a diameter so the difference is deliberate). - Station labels are sentence case with their recommended symbols (medal, tag, location.north.fill, trophy) and StationLabelKind replaces the Key's string-literal dispatch, so renaming a label can no longer drop a row. - Key says "Costs", matching the capsule's own headline (was "Costs more").
This commit is contained in:
@@ -11,9 +11,6 @@ import WidgetKit
|
||||
/// takes the two nested-sheet flags as bindings, so each tab keeps owning its own
|
||||
/// QA launch-argument hooks.
|
||||
struct KeySheetView: View {
|
||||
/// The selected fuel's colour. `KeyStationLabelIndicator` draws its pump badge
|
||||
/// in it, so the legend matches the rows the reader is looking at.
|
||||
let fuelTint: Color
|
||||
@Binding var showBestValueInfo: Bool
|
||||
@Binding var showImportantInformation: Bool
|
||||
|
||||
@@ -47,9 +44,9 @@ struct KeySheetView: View {
|
||||
}
|
||||
|
||||
Section {
|
||||
ForEach(KeySheetContent.stationLabels, id: \.label) { item in
|
||||
KeySheetDescriptionRow(item: item) {
|
||||
KeyStationLabelIndicator(label: item.label, fuelColor: fuelTint)
|
||||
ForEach(StationLabelKind.allCases, id: \.self) { kind in
|
||||
KeySheetDescriptionRow(item: item(for: kind)) {
|
||||
KeyStationLabelIndicator(kind: kind)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -87,6 +84,16 @@ struct KeySheetView: View {
|
||||
.presentationDetents([.fraction(0.6), .large])
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
|
||||
/// The Key row's copy for a label kind. Fails loudly in debug rather than
|
||||
/// silently rendering a blank row if a kind is ever added without its entry.
|
||||
private func item(for kind: StationLabelKind) -> KeySheetContent.Item {
|
||||
if let match = KeySheetContent.stationLabels.first(where: { $0.label == kind.label }) {
|
||||
return match
|
||||
}
|
||||
assertionFailure("No KeySheetContent.stationLabels entry for \(kind)")
|
||||
return KeySheetContent.Item(label: kind.label, description: "")
|
||||
}
|
||||
}
|
||||
|
||||
/// Stations tab — the main list with fuel picker, distance filter, sort, pagination.
|
||||
@@ -384,12 +391,11 @@ struct StationsView: View {
|
||||
)
|
||||
}
|
||||
|
||||
/// The legend sheet, shared with the Favourites tab. Only the fuel differs:
|
||||
/// its colour tints the station-label row, so the legend matches the rows the
|
||||
/// reader is actually looking at.
|
||||
/// The legend sheet, shared with the Favourites tab. Nothing varies by fuel:
|
||||
/// every station label has a fixed semantic tone, so the legend reads the same
|
||||
/// whichever grade the reader is looking at.
|
||||
private var keySheet: some View {
|
||||
KeySheetView(
|
||||
fuelTint: selectedFuel.tintColor,
|
||||
showBestValueInfo: $showBestValueInfo,
|
||||
showImportantInformation: $showImportantInformation
|
||||
)
|
||||
@@ -473,10 +479,12 @@ private struct KeySheetDescriptionRow<Indicator: View>: View {
|
||||
private struct KeySavingsCapsule: View {
|
||||
let label: String
|
||||
|
||||
/// Keyed on the shared label so the legend cannot demonstrate a word the
|
||||
/// listing capsule never draws.
|
||||
private var status: FuelSavingsStatus {
|
||||
switch label {
|
||||
case "Saves": return .worthTheDrive
|
||||
case "Costs more": return .notWorthTheDrive
|
||||
case String(localized: "Saves"): return .worthTheDrive
|
||||
case String(localized: "Costs"): return .notWorthTheDrive
|
||||
default: return .breakEven
|
||||
}
|
||||
}
|
||||
@@ -502,13 +510,23 @@ private struct KeySavingsCapsule: View {
|
||||
}
|
||||
|
||||
/// Shared RAG dot used beside live station prices and in the Key.
|
||||
///
|
||||
/// 12pt is the app's one diameter: it reads at a glance beside a 22pt price
|
||||
/// without competing with it, and it is large enough that the three colours are
|
||||
/// distinguishable for colour-vision-deficient users at arm's length. The widget
|
||||
/// keeps a smaller dot of its own — its rows are far tighter than a phone row —
|
||||
/// so this size is deliberately the APP's.
|
||||
struct PriceRatingDot: View {
|
||||
let color: Color
|
||||
var diameter: CGFloat = PriceRatingDot.standardDiameter
|
||||
|
||||
/// The app's standard dot diameter, in points.
|
||||
static let standardDiameter: CGFloat = 12
|
||||
|
||||
var body: some View {
|
||||
Circle()
|
||||
.fill(color)
|
||||
.frame(width: 8, height: 8)
|
||||
.frame(width: diameter, height: diameter)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
}
|
||||
@@ -525,43 +543,51 @@ struct FavouriteStarGlyph: View {
|
||||
|
||||
/// Shared station-label capsule. `StationRow` uses the same view for its live
|
||||
/// BEST VALUE/CHEAPEST badge, so the Key cannot drift from the listing style.
|
||||
///
|
||||
/// One tone per label kind, from the shared palette: purple for best value, blue
|
||||
/// for cheapest and top (a top favourite IS the cheapest favourite, so they share
|
||||
/// a class), neutral for proximity. The symbol is the label's own meaning, sized
|
||||
/// to sit on the caption's cap height rather than to dominate it.
|
||||
struct StationLabelBadge: View {
|
||||
let label: String
|
||||
let color: Color
|
||||
let kind: StationLabelKind
|
||||
|
||||
private var tone: CapsuleTone {
|
||||
switch kind {
|
||||
case .bestValue: return .bestValue
|
||||
case .cheapest: return .cheapest
|
||||
case .closestStation: return .closestStation
|
||||
case .top: return .top
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(LocalizedStringKey(label))
|
||||
.font(.caption2.bold())
|
||||
.foregroundStyle(color)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(Capsule().fill(color.opacity(0.15)))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: kind.symbol)
|
||||
.font(.caption2.weight(.semibold))
|
||||
.accessibilityHidden(true)
|
||||
Text(LocalizedStringKey(kind.label))
|
||||
.font(.caption.weight(.semibold))
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
}
|
||||
.foregroundStyle(tone.foreground)
|
||||
.padding(.horizontal, 7)
|
||||
.padding(.vertical, 3)
|
||||
.background(Capsule().fill(tone.background))
|
||||
}
|
||||
}
|
||||
|
||||
/// The Key sheet's indicator for a station label. The closest station is the one
|
||||
/// exception: it renders the listing's real verdict capsule, so the legend
|
||||
/// demonstrates the same fill, text and shape the rows show. Everything else is
|
||||
/// the shared badge.
|
||||
private struct KeyStationLabelIndicator: View {
|
||||
let label: String
|
||||
let fuelColor: Color
|
||||
let kind: StationLabelKind
|
||||
|
||||
var body: some View {
|
||||
switch label {
|
||||
case "BEST VALUE":
|
||||
StationLabelBadge(label: label, color: .green)
|
||||
case "CHEAPEST":
|
||||
StationLabelBadge(label: label, color: .blue)
|
||||
case "CLOSEST":
|
||||
// The listing's own capsule, so the Key demonstrates the same soft
|
||||
// blue fill and white text the listing shows. `FuelSavingsCapsule`
|
||||
// remains the detail sheet's alone.
|
||||
if kind == .closestStation {
|
||||
ListingFuelSavingsCapsule(result: baselineResult)
|
||||
default:
|
||||
Text("TOP")
|
||||
.font(.caption2.bold())
|
||||
.padding(.horizontal, 4)
|
||||
.padding(.vertical, 2)
|
||||
.background(fuelColor.opacity(0.18), in: Capsule())
|
||||
.fixedSize(horizontal: true, vertical: false)
|
||||
} else {
|
||||
StationLabelBadge(kind: kind)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user