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:
@@ -271,17 +271,51 @@ final class KeySheetContentTests: XCTestCase {
|
||||
XCTAssertEqual(KeySheetContent.savings, [
|
||||
.init(label: "Saves", description: "You’ll save money compared with your closest station, including travel costs."),
|
||||
.init(label: "Break even", description: "The saving or extra cost is negligible."),
|
||||
.init(label: "Costs more", description: "Filling up here costs more than at your closest station, including travel.")
|
||||
// "Costs" — the listing capsule's own headline, not "Costs more".
|
||||
.init(label: "Costs", description: "Filling up here costs more than at your closest station, including travel.")
|
||||
])
|
||||
XCTAssertFalse(KeySheetContent.savings.contains { $0.label == "Small saving" })
|
||||
}
|
||||
|
||||
/// Every Key label is sentence case: the capsules are read as phrases, and an
|
||||
/// all-caps set reads as shouting beside the sentence-case copy around it.
|
||||
func testEveryKeyLabelIsSentenceCase() {
|
||||
let labels = (KeySheetContent.priceRatings
|
||||
+ KeySheetContent.savings
|
||||
+ KeySheetContent.stationLabels).map(\.label)
|
||||
for label in labels {
|
||||
XCTAssertNotEqual(label, label.uppercased(),
|
||||
"\(label) must be sentence case, not all caps")
|
||||
}
|
||||
XCTAssertEqual(KeySheetContent.stationLabels.map(\.label),
|
||||
["Best value", "Cheapest", "Closest station", "Top"])
|
||||
}
|
||||
|
||||
/// The Key dispatches its indicators on `StationLabelKind`, so every kind must
|
||||
/// have exactly one entry in the legend — and each entry's label must be the
|
||||
/// kind's own, or the row would describe one label and draw another.
|
||||
func testEveryStationLabelKindHasOneMatchingLegendEntry() {
|
||||
XCTAssertEqual(StationLabelKind.allCases.count, KeySheetContent.stationLabels.count)
|
||||
for kind in StationLabelKind.allCases {
|
||||
let matches = KeySheetContent.stationLabels.filter { $0.label == kind.label }
|
||||
XCTAssertEqual(matches.count, 1, "\(kind) must own exactly one legend entry")
|
||||
}
|
||||
}
|
||||
|
||||
/// Recommended symbols, pinned so a rename cannot silently blank a badge.
|
||||
func testStationLabelKindsUseTheirRecommendedSymbols() {
|
||||
XCTAssertEqual(StationLabelKind.bestValue.symbol, "medal")
|
||||
XCTAssertEqual(StationLabelKind.cheapest.symbol, "tag")
|
||||
XCTAssertEqual(StationLabelKind.closestStation.symbol, "location.north.fill")
|
||||
XCTAssertEqual(StationLabelKind.top.symbol, "trophy")
|
||||
}
|
||||
|
||||
func testStationLabelsDescribeTheCurrentQualificationAndRangeRules() {
|
||||
XCTAssertEqual(KeySheetContent.stationLabels, [
|
||||
.init(label: "BEST VALUE", description: "Meets your minimum saving after travel costs."),
|
||||
.init(label: "CHEAPEST", description: "The lowest fuel price per litre within your selected range."),
|
||||
.init(label: "CLOSEST", description: "The nearest station by journey distance."),
|
||||
.init(label: "TOP", description: "The cheapest station in your Favourites for the selected fuel.")
|
||||
.init(label: "Best value", description: "Meets your minimum saving after travel costs."),
|
||||
.init(label: "Cheapest", description: "The lowest fuel price per litre within your selected range."),
|
||||
.init(label: "Closest station", description: "The nearest station by journey distance."),
|
||||
.init(label: "Top", description: "The cheapest station in your Favourites for the selected fuel.")
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user