Skip to content

feat(mobile): Material iconography, icon tiles and a rebuilt dashboard - #141

Merged
rghvgrv merged 4 commits into
mainfrom
feat/material-icons-and-colorful-ui
Aug 8, 2026
Merged

feat(mobile): Material iconography, icon tiles and a rebuilt dashboard#141
rghvgrv merged 4 commits into
mainfrom
feat/material-icons-and-colorful-ui

Conversation

@rghvgrv

@rghvgrv rghvgrv commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Visual pass on the mobile app. Branched from current main (a20b3e0), so #140 is included.

1. Material icons

Replaced the ad-hoc geometries with Material Design's own 24dp path data, and grew the set from 9 icons to 23 — one per system category, one per payment-source type, plus interface icons.

Path data rather than the MaterialIcons font, which is a real trade worth your call:

  • A Path takes a theme-bound Fill. A font glyph is a Label whose colour has to be set at every call site — the existing theme is built entirely on AppThemeBinding pairs, and the font would have opted every icon out of that.
  • No binary font asset or licence file in the repo.
  • The shapes are identical either way.

Also: this machine has no outbound TLS (revocation check fails), so I couldn't fetch the TTF — shipping it would have meant leaving you a manual download step. Say the word if you'd rather have the real font and I'll wire it up with the file committed.

2. Icon tiles — the "bold square" ask

A filled rounded square (42px, 13px radius) carrying one glyph, used wherever a row identifies something: categories, payment sources, dashboard breakdown rows.

The wash behind the glyph is the item's own colour at 20% alpha; the glyph is that colour at full strength. Contrast holds in both themes without a second palette.

Square, not circular, deliberately — the subscription list already uses circles for provider logos. An avatar and a category marker shouldn't look like the same kind of thing.

This replaces the 10px dot beside category names, where hue was the only thing separating one row from the next. A colour can't say "Fitness"; a dumbbell can.

3. Dashboard

The three secondary figures moved out of the hero — where they were three more lines of white text competing with the headline — into their own tiles, each with an icon and its own accent. The hero keeps the weekly figure and gains a currency badge in translucent white rather than a second brand colour.

4. Colour

Success plus three tile accents added as theme-bound token pairs; Android's colorPrimaryDark/colorAccent moved off the near-black they were set to. Every new colour is an AppThemeBinding pair — a single-theme colour becomes unreadable the moment the phone flips theme, which on Android is one swipe away.

Behaviour kept deliberately

Icons fall back rather than guess. A dinner plate against "Pet insurance" reads as a bug; a neutral marker reads as "no icon for this". User-created categories always hit the fallback, since nothing can infer an icon from an arbitrary name.

Testing

185 mobile tests pass (19 new). The new tests cover the mappings, case-insensitivity, fallbacks, and one invariant worth having: category icons and category colours must cover the same set, so a category can't end up with a hue and a generic glyph.

⚠️ Not visually verified

The Android TFM needs a JDK this machine doesn't have (XA5300), so the XAML was compiled through the Windows TFM — same XAML source generator — and the tests pass. Nobody has actually looked at it on a phone.

"Beautiful" is your call, not mine. Please run it on a device before merging, and tell me what to adjust — tile size, corner radius, accent hues, spacing are all one-line changes in Theme.xaml.

Not touched

SubscriptionListPage and SubscriptionDetailPage still use their existing treatment (circular provider logos, chips). They're the next candidates if you like this direction, but they're the screens with the most working detail and I didn't want to churn them speculatively in the same pass.

rghvgrv and others added 4 commits August 8, 2026 15:14
The app read as a wall of text with a coloured header. Four changes.

Material icon set. The existing ad-hoc geometries are replaced with Material
Design's own 24dp path data, and the set grows from nine icons to twenty-three -
one per system category, one per payment-source type, plus the interface icons.

Transcribed as path data rather than shipped as the MaterialIcons font, and that
is a real trade worth stating: a Path takes a theme-bound Fill, where a font
glyph is a Label whose colour has to be set at every call site. It also keeps the
app free of a binary font asset and its licence file. The shapes are the same
either way. (The font could not be fetched here regardless - this machine has no
outbound TLS - so shipping it would have meant hand-waving a download step.)

Icon tiles. A filled rounded square carrying one glyph, wherever a row
identifies something: categories, payment sources, the dashboard breakdown. The
wash behind the glyph is the item's own colour at 20% and the glyph is that
colour at full strength, so contrast holds in both themes without a second
palette.

Square, not circular, on purpose - the subscription list already uses circles for
provider logos, and an avatar and a category marker must not look like the same
kind of thing.

This replaces the 10px dot that used to sit beside category names, where hue was
the only thing separating one row from the next. A colour cannot say "Fitness";
a dumbbell can.

Dashboard. The three secondary figures moved out of the hero, where they were
three more lines of white text competing with the headline, into their own tiles
with an icon and accent each. The hero keeps the weekly figure and gains a
currency badge in translucent white rather than a second brand colour.

Colour. Success and three tile accents added as theme-bound token pairs, and the
Android colorPrimaryDark/colorAccent moved off the near-black they were set to.
Every new colour is an AppThemeBinding pair - a single-theme colour is
unreadable text the moment the phone is in the other one.

Category and payment icons deliberately fall back rather than guess: a dinner
plate against "Pet insurance" reads as a bug where a neutral marker reads as
"no icon for this". A test holds the icon and colour maps against each other, so
a category cannot end up with a hue and no glyph.

Not visually verified. The Android TFM needs a JDK this machine does not have,
so the XAML was compiled through the Windows TFM - same source generator - and
the 185 mobile tests pass. Someone should look at it on a device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Amounts printed the three-letter code next to the number - "USD 15.99",
"INR 1,699.50". A symbol is what people read money as, and on a lock-screen
notification "₹1,699.50" lands where "1,699.50 INR" has to be parsed.

Resolved from the code carried on each amount, not from CultureInfo. The device
culture describes the phone, not the money: on a handset set to en-US,
NumberFormat.CurrencySymbol renders a rupee-billed subscription with a dollar
sign. The app is multi-currency by design - a dollar subscription sits in the
same list as a rupee one, and the dashboard converts across them - so
hardcoding ₹ would have been wrong in exactly the places that matter.

The table is deliberately not exhaustive. A code earns a symbol only when that
symbol identifies it on sight; anything ambiguous keeps its letters, because
"kr" against SEK, NOK and DKK, or ₨ against PKR and LKR, tells the reader less
than the code does. The dollar family is disambiguated the way CLDR does it -
US$ bare, everyone else prefixed - since a lone $ beside a converted total is
the one ambiguity worth spending characters on. Unknown codes fall through
unchanged, so the worst case is what the app printed before.

Entry fields on the detail and settings screens still take ISO codes: that is
the stored value and what the API validates.

Also aligns the burn-rate fallback with the schema. users.preferred_currency has
defaulted to INR since DefaultPreferredCurrencyInr, while DashboardController
still fell back to USD. It only fires if the row vanished mid-request, but two
defaults disagreeing is the kind of thing that surfaces years later as one
account inexplicably totalling in dollars.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…g a code

Settings asked for a three-letter code in a free-text Entry, which means knowing
the code, and a typo only surfaced as a validation error after Save. It is now a
picker showing symbol, code and name together - "₹  INR — Indian Rupee" - while
the stored value stays the ISO code the API expects.

The list is derived from the runtime's own region data, the same source
SubVora.Application.CurrencyCodes validates against on the server, so the picker
cannot offer something the API would reject. Sorted ordinally by code: stable,
and independent of the device language, since a picker that reorders itself
because the phone's locale changed would be its own bug report.

INR remains the default, from users.preferred_currency - this only changes how
the value is chosen, not what it starts as.

Two cases worth calling out:

- A stored code the runtime does not know - saved by another client, or since
  retired - is inserted into the list at its sorted position and selected. It
  would otherwise leave the picker on nothing, and any later Save would quietly
  rewrite the currency the user actually had. A test covers it, and caught the
  first attempt resolving the selection against the global list rather than the
  bound one.
- A profile that fails to load leaves the selection empty rather than defaulting
  to INR. Showing a currency the user never chose, on a screen whose Save button
  is one tap away, would overwrite whatever is really stored.

PreferredCurrency stays the source of truth and the two properties update each
other, guarded so the handlers do not recurse.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e button

Add, edit, delete and profile-save all failed offline with "You appear to be
offline." - true, and useless on a write. It leaves the reader to guess whether
the change was stored locally and will sync later. It will not: the SQLite
mirror is refreshed from successful GETs only and there is no write queue, so
the change is discarded. The message now says that.

ApiErrorMapper gains ToWriteFailureMessage, used only on write paths. Reads keep
the old wording, because nothing was lost on a read - there is just nothing new
to show. Login and register keep it too: a failed sign-in is not a discarded
edit.

IConnectivityService was registered in MauiProgram and had no consumers at all.
The four write-capable view models now take it, expose IsOffline, and gate their
button through CanSubmit, which folds in IsBusy so a second tap cannot land
while the first write is running. Each screen also carries a short notice while
offline, so the disabled button has a reason next to it.

Connectivity is read when the screen loads and again after a failed write,
rather than by subscribing to the service's event: these view models are
transient while IConnectivityService is a singleton, so a subscription would
outlive the screen it belongs to.

Worth being precise about what this does and does not catch. IsConnected means
"this phone has a network", not "the server is reachable" - the case that
started this was wifi up and the local API down, which still reads as online and
keeps the button enabled. The message is what carries the truth there, and a
test pins exactly that scenario.

This is deliberately not offline write support. Queuing changes needs an outbox,
replay on reconnect, and conflict handling against the version token added in
#138 - a queued edit can hit the same stale-version 409. Until that exists, the
honest behaviour is to fail loudly rather than look like it saved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@rghvgrv
rghvgrv merged commit a3a5b88 into main Aug 8, 2026
3 checks passed
@rghvgrv
rghvgrv deleted the feat/material-icons-and-colorful-ui branch August 8, 2026 12:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant