Make Closest a station label and retire Top
Top and Cheapest were two words for one state: both meant "the lowest price among the stations on screen", differing only in the candidate set (radius vs Favourites). Which word a surface used had drifted by device — the phone's rows said Cheapest while the watch said Top for the identical state. Top is gone from the enum, the Key legend, the badge and the watch's hand-rolled label; Cheapest survives because it names the measure and is already the word the favourites copy uses. Closest is now a real station label rather than a verdict. StationRecommendationBadge gains .closest with a total precedence — Best value, then Cheapest, then Closest — because a row has room for one label and proximity is the weakest claim: a short drive says nothing about the price, and unlike the other two it reports no saving at all. The badge sits above the price, where the other two sit. The closest row's verdict capsule is suppressed: its only word for that state was "Closest", so printing it below as well would state the same fact twice, three lines apart. Other rows keep their Saves / Break even / Costs capsules unchanged. "Closest station" becomes "Closest", read from the single FuelSavingsResult.baselineHeadline constant so the badge, the verdict and the Key cannot word it differently. The Key legend renders all three station labels with the shared StationLabelBadge; the old KeyStationLabelIndicator, which special-cased the closest station to a wider verdict pill so it read as a different class of thing from the two above it, is deleted. One closest icon everywhere. The listing and detail verdict symbols used location.circle and location.circle.fill for the state the Key taught as location.north.fill; both now read StationLabelKind.closest.symbol, so the three surfaces resolve one glyph from one place. The detail sheet gains the Closest capsule below the map, ordered to match the Key. Favourites gains the nearest favourite as its Closest, measured on the same one-way distance its rows print and restricted to favourites with a price for the active fuel — matching the radius tab, where the closest station is by definition the savings baseline. closestStationID flows from the one scaffold baseline the savings lookup already compares against, so a row's badge and the reference station cannot disagree. No savings calculation, threshold, ranking, eligibility or station-selection logic changed. The Key sheet's detent is now bound rather than fixed, so dragging it up to read the Station labels section sticks instead of snapping back; a QA launch argument opens it at full height, since the 0.6 detent puts that section below the fold on every device. That argument is read unconditionally, unlike the other harness hooks which are #if DEBUG-gated. Tests: 278 pass, 9 new — the label precedence ladder across every combination, capsule order in the sheet, Closest alone, and the retired label pinned absent.
This commit is contained in:
@@ -15,6 +15,16 @@ struct KeySheetView: View {
|
||||
@Binding var showImportantInformation: Bool
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
/// Where the user last dragged the sheet. Bound rather than fixed at 0.6 so
|
||||
/// dragging it up to read the Station labels section sticks, instead of
|
||||
/// snapping back every time the sheet is reopened.
|
||||
///
|
||||
/// The QA launch argument sets the INITIAL detent rather than assigning one
|
||||
/// after the sheet appears: assigning a new selection while the sheet is still
|
||||
/// presenting is ignored, which left the legend stuck at 0.6 and the Station
|
||||
/// labels section off screen.
|
||||
@State private var detent: PresentationDetent =
|
||||
ProcessInfo.processInfo.arguments.contains("-qaKeySheetLarge") ? .large : .fraction(0.6)
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
@@ -46,7 +56,7 @@ struct KeySheetView: View {
|
||||
Section {
|
||||
ForEach(StationLabelKind.allCases, id: \.self) { kind in
|
||||
KeySheetDescriptionRow(item: item(for: kind)) {
|
||||
KeyStationLabelIndicator(kind: kind)
|
||||
StationLabelBadge(kind: kind)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
@@ -81,7 +91,11 @@ struct KeySheetView: View {
|
||||
// Nested sheet: the Key sheet stays behind the disclaimer.
|
||||
.importantInformationSheet(isPresented: $showImportantInformation)
|
||||
}
|
||||
.presentationDetents([.fraction(0.6), .large])
|
||||
// QA hook: `-qaKeySheetLarge` opens the legend at full height — see the
|
||||
// `detent` state. The 0.6 detent puts the Station labels section below the
|
||||
// fold on every device, including the tallest iPhone, so the three label
|
||||
// badges cannot otherwise be seen on screen.
|
||||
.presentationDetents([.fraction(0.6), .large], selection: $detent)
|
||||
.presentationBackground(Color(UIColor.systemGroupedBackground))
|
||||
}
|
||||
|
||||
@@ -107,7 +121,11 @@ struct StationsView: View {
|
||||
let distanceUnit: DistanceUnit
|
||||
let priceDisplayStyle: PriceDisplayStyle
|
||||
let baselinePrice: Double?
|
||||
let topStationID: String?
|
||||
let cheapestStationID: String?
|
||||
/// The station the savings are measured against — the closest station with an
|
||||
/// eligible price. Passed in from the owner of the scaffold baseline so this
|
||||
/// list's Closest badge and the reference station cannot disagree.
|
||||
let closestStationID: String?
|
||||
let location: Coordinate?
|
||||
let favouriteIDs: Set<String>
|
||||
let stationTapAction: StationTapAction
|
||||
@@ -119,7 +137,7 @@ struct StationsView: View {
|
||||
var onRefresh: () async -> Void = {}
|
||||
|
||||
/// Pagination: one page = 10 rows, reset whenever the underlying list
|
||||
/// changes (new fetch, fuel/sort/radius switch). RAG/TOP/deltas still come
|
||||
/// changes (new fetch, fuel/sort/radius switch). RAG/badge/deltas still come
|
||||
/// from the whole radius pool via `cheapestPrice` — paging never changes
|
||||
/// which station is "best".
|
||||
@State private var pageSize = 10
|
||||
@@ -386,8 +404,10 @@ struct StationsView: View {
|
||||
fuelSavings: savingsForStation(station),
|
||||
fuelSavingsAmount: fuelSavingsAmount,
|
||||
journeyDiagnostics: journeyDiagnosticsForStation(station),
|
||||
// The same flag the row gets from `topStationID`.
|
||||
isCheapestInRange: station.id == topStationID,
|
||||
// The same flag the row gets from `cheapestStationID`.
|
||||
isCheapestInRange: station.id == cheapestStationID,
|
||||
// The same flag the row gets from `closestStationID`.
|
||||
isClosestInRange: station.id == closestStationID,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
onToggleFavourite: { onToggleFavourite(station, selectedFuel) }
|
||||
)
|
||||
@@ -411,7 +431,8 @@ struct StationsView: View {
|
||||
distanceUnit: distanceUnit,
|
||||
priceDisplayStyle: priceDisplayStyle,
|
||||
baselinePrice: baselinePrice,
|
||||
isTopResult: station.id == topStationID,
|
||||
isCheapestResult: station.id == cheapestStationID,
|
||||
isClosestResult: station.id == closestStationID,
|
||||
isFavourite: favouriteIDs.contains(station.id),
|
||||
tapAction: stationTapAction,
|
||||
fuelSavings: savingsForStation(station),
|
||||
@@ -557,8 +578,7 @@ struct StationLabelBadge: View {
|
||||
switch kind {
|
||||
case .bestValue: return .bestValue
|
||||
case .cheapest: return .cheapest
|
||||
case .closestStation: return .closestStation
|
||||
case .top: return .top
|
||||
case .closest: return .closest
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,34 +598,14 @@ struct StationLabelBadge: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 kind: StationLabelKind
|
||||
|
||||
var body: some View {
|
||||
if kind == .closestStation {
|
||||
ListingFuelSavingsCapsule(result: baselineResult)
|
||||
} else {
|
||||
StationLabelBadge(kind: kind)
|
||||
}
|
||||
}
|
||||
|
||||
private var baselineResult: FuelSavingsResult {
|
||||
FuelSavingsResult(
|
||||
fillLitres: 30,
|
||||
grossSavingGBP: 0,
|
||||
extraDrivingCostGBP: 0,
|
||||
extraDistanceMiles: 0,
|
||||
netSavingGBP: 0,
|
||||
status: .baseline,
|
||||
journeyConfidence: .confirmed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Every station label in the Key is the SAME `StationLabelBadge` the listing rows
|
||||
/// and the More info sheet draw.
|
||||
///
|
||||
/// The closest station used to be the exception — the legend rendered the real
|
||||
/// verdict capsule for it, a wider pill with different fill and shape, so it read as
|
||||
/// a different class of thing from Best value and Cheapest sitting above it. Now
|
||||
/// that its label is the single word "Closest", it belongs in the same column, in
|
||||
/// the same capsule, at the same size.
|
||||
// MARK: - Fuel-type iconography (app target only — FuelStore.swift is Foundation-only)
|
||||
|
||||
extension FuelType {
|
||||
|
||||
Reference in New Issue
Block a user