Miler rider app: surface system, visible design language, backend lifecycle
Design system - MilerSurface ladder (canvas → working → raised → floating) with MilerPanel as layer 1; canvas moved to #DEE3EA so white separates at 1.290:1. - Visible vocabulary applied across Home, Deliveries, Activity, Account and the sheets: hero heads (tabular numeral + small caption, clamped at 1.3x), canvas wells for anything that opens, small filled tags for shelf labels, demoted placeholders. Recorded in DESIGN_SYSTEM.md §6. - One icon family: 222 Material glyphs migrated to Lucide; none left outside lib/xpress. - Colour semantics corrected: amber only for what is genuinely owed, brand red reserved for the live stop, disabled primaries go neutral rather than pale. Data and lifecycle - lib/data/lifecycle.dart reads mutations for what they prove; route_order.dart makes admin sequence the single ordering authority; service_day.dart, and stop_area.dart rewritten against live Coimbatore addresses (digit-token stripping, city stoplist, street suffixes, stammer collapse). - countLabel states the load once, in bags. Testing - 1440 tests passing; golden shot harnesses for Home, Deliveries, Activity, sheets and verify, with test/failures/ now gitignored (diff debris). - New pins: home_gutter_test, stop_area_test, plus updated structural bounds. Note: this commit also carries pre-existing working-tree deletions that were present before this work (API_SPEC.md, README.md, demo test fixtures). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
128
test/surface_system_test.dart
Normal file
128
test/surface_system_test.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:miler/views/helpers/constants/Colorconstants.dart';
|
||||
import 'package:miler/views/helpers/constants/miler_surface.dart';
|
||||
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
/// THE LAYERS HAVE TO BE VISIBLE TO BE LAYERS
|
||||
///
|
||||
/// The app looked flat for a measurable reason, not an aesthetic one: white on
|
||||
/// the page ground was a **1.124 : 1** step. A tonal difference does not begin
|
||||
/// to read as a *different surface* until roughly 1.25, and on the budget LCD
|
||||
/// this app ships to it needs the top of that band. So every screen that
|
||||
/// wanted its content to separate grew a border or a shadow to do the ground's
|
||||
/// job — which is how an app ends up looking like a stack of rounded
|
||||
/// rectangles while its ground does nothing.
|
||||
///
|
||||
/// There were also five near-identical light tokens, three of them within
|
||||
/// 1.03 of the canvas: indistinguishable, and therefore five names for two
|
||||
/// decisions.
|
||||
///
|
||||
/// These pin the arithmetic. A contrast rule that lives only in a comment is a
|
||||
/// rule that gets undone by the next person who nudges a hex.
|
||||
/// ─────────────────────────────────────────────────────────────────────────
|
||||
double _luminance(Color c) {
|
||||
double channel(double v) =>
|
||||
v <= 0.03928 ? v / 12.92 : math.pow((v + 0.055) / 1.055, 2.4).toDouble();
|
||||
return 0.2126 * channel(c.r) + 0.7152 * channel(c.g) + 0.0722 * channel(c.b);
|
||||
}
|
||||
|
||||
double contrast(Color a, Color b) {
|
||||
final la = _luminance(a);
|
||||
final lb = _luminance(b);
|
||||
final hi = math.max(la, lb);
|
||||
final lo = math.min(la, lb);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
void main() {
|
||||
group('the ladder has real rungs', () {
|
||||
test('a working surface separates from the canvas without a border', () {
|
||||
final step = contrast(MilerSurface.working, MilerSurface.canvas);
|
||||
expect(
|
||||
step,
|
||||
greaterThanOrEqualTo(1.25),
|
||||
reason:
|
||||
'white on the canvas is $step : 1 — below ~1.25 a surface does '
|
||||
'not read as a surface, and screens start drawing their own '
|
||||
'outlines to compensate',
|
||||
);
|
||||
// And not so far that the page turns into a dark frame around content.
|
||||
expect(step, lessThan(1.6));
|
||||
});
|
||||
|
||||
test('the canvas is never white', () {
|
||||
expect(MilerSurface.canvas, isNot(MilerSurface.working));
|
||||
expect(MilerSurface.canvas, isNot(const Color(0xFFFFFFFF)));
|
||||
});
|
||||
|
||||
test('raised and floating carry lift, working does not', () {
|
||||
// What separates an action surface from a reading surface is that it
|
||||
// appears to sit above the page — the fill is the same on purpose.
|
||||
expect(MilerSurface.raisedShadow, isNotEmpty);
|
||||
expect(MilerSurface.floatingShadow, isNotEmpty);
|
||||
expect(MilerSurface.raised, MilerSurface.working);
|
||||
});
|
||||
|
||||
test('the scrim carries the modal separation', () {
|
||||
// A sheet that needs a heavy shadow to be legible is a sheet whose scrim
|
||||
// is too weak.
|
||||
expect(MilerSurface.scrim.a, greaterThan(0.3));
|
||||
});
|
||||
});
|
||||
|
||||
group('text stays readable on every layer', () {
|
||||
// AA body is 4.5:1. Every operational colour is asserted on the canvas —
|
||||
// the darkest ground text sits on — so a screen cannot fail by moving
|
||||
// content from a card onto the page.
|
||||
final grounds = <String, Color>{
|
||||
'canvas': MilerSurface.canvas,
|
||||
'working': MilerSurface.working,
|
||||
};
|
||||
final inks = <String, Color>{
|
||||
'slateText': ColorConstants.slateText,
|
||||
'secondaryText': ColorConstants.secondaryText,
|
||||
'onSurfaceVariant': ColorConstants.onSurfaceVariant,
|
||||
'primary': ColorConstants.primary,
|
||||
'acceptGreen': ColorConstants.acceptGreen,
|
||||
'warning': ColorConstants.warning,
|
||||
'errorRed': ColorConstants.errorRed,
|
||||
};
|
||||
|
||||
for (final g in grounds.entries) {
|
||||
for (final ink in inks.entries) {
|
||||
test('${ink.key} on ${g.key} clears AA', () {
|
||||
final r = contrast(ink.value, g.value);
|
||||
expect(
|
||||
r,
|
||||
greaterThanOrEqualTo(4.5),
|
||||
reason: '${ink.key} on ${g.key} is ${r.toStringAsFixed(2)} : 1',
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
group('the near-duplicate tokens are gone or justified', () {
|
||||
test('the inset tone is distinguishable from the surface it sits on', () {
|
||||
// The only nesting the system allows. If it cannot be seen against the
|
||||
// working surface it is furniture, not a layer.
|
||||
final step = contrast(MilerSurface.inset, MilerSurface.working);
|
||||
expect(step, greaterThan(1.05));
|
||||
});
|
||||
|
||||
test('brand red is identity, error red is not the same colour', () {
|
||||
// Red must not mean two things. Navigation/primary and failure have to
|
||||
// be tellable apart at a glance, outdoors.
|
||||
expect(ColorConstants.primary, isNot(ColorConstants.errorRed));
|
||||
expect(
|
||||
contrast(ColorConstants.primary, ColorConstants.errorRed),
|
||||
greaterThan(1.15),
|
||||
reason: 'brand red and error red must not read as the same ink',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user