Skip to content

Flags and startup: a flag on every platform, a faster first paint, the screen kept on reload - #36

Merged
davd-gzl merged 21 commits into
mainfrom
ux-flags-map
Sep 30, 2026
Merged

davd-gzl merged 21 commits into
mainfrom
ux-flags-map

Conversation

@davd-gzl

Copy link
Copy Markdown
Collaborator

Based on #31, whose branch it builds on; merge that first.

On Windows and on a Linux without a colour-emoji font, every country flag in the app renders as two letters or an empty box, because flags are Unicode flag characters and those systems have no glyphs for them. The app now ships the Twemoji country flags as a 78 KB font limited to the flag characters, so a browser fetches it only once a flag is on screen, and the service worker keeps it for offline use. Its CC-BY licence and source sit beside it in public/fonts, and Settings credits it with the other data sources. Search results and the map's place card now lead with the flag, and at world zoom a country's own flags stack into one while they would overlap, so Europe no longer piles up.

On a phone over 4G with a slow processor, the first screen painted after 7.6 seconds, because MapLibre and five reference files loaded before anything drew. MapLibre now loads with the map, the top bar paints from the HTML at once, and only the city and region lists are awaited; airports, monuments and the rest merge in after the first render. First paint went from 7.6 to 1.6 seconds and the app is usable at 4.9 instead of 8.5, the median of three runs each. The bytes transferred stay the same, only their order changed.

A reload used to land on the map whatever screen was open. The tab and the open city or country page now sit in the address, so a reload, Back and Forward return to them, while the home screen keeps its bare address.

The backup reminder also heads Places, not only the middle of Settings. On a 375 px phone the search field was 85 px wide and showed "Sea"; the brand collapses to its pin mark there, which gives the field 158 px. The search, star and map-mode icons are drawn as SVG, so they no longer vanish without an emoji font.

On Windows and minimal Linux a regional-indicator pair has no colour glyph,
so the Places list lost its flags and the map's flag markers turned into
empty boxes. A flag-only Twemoji font (78 KB woff2, CC-BY 4.0) now leads the
page and canvas font stacks behind a unicode-range, so it is fetched only
once a flag shows, precached for offline use, and the map redraws its flag
markers once it has loaded. Credited in Settings' data sources.
Every place list showed the country's flag beside a name except the global
search results and the map's place card, where the country was named
without one. Both lead with it now, hidden from assistive tech so the
option and card keep the place's name. The card's country line also lost
the stray leading dot it carried when opened from a list or a search.
At world zoom a country's visited cities sat on top of each other, so
Europe became a pile of identical flags over the neighbours' ones, a
logged airport's pill covered its country's only flag, and with markers
allowed to overlap the sort key drew favourites at the bottom of the pile.
A country's own flags now stack into one while they would overlap (a
favourite or custom place always keeps its own), each country's main city
outranks any second city, the pile's top is its highest priority, airport
pills sit under the flags and shrink at world zoom, and the flag glyph is
sized to fill its marker.
At 375px the brand's wordmark and four action buttons left the search field
85px wide, its placeholder clipped to "Sea"; below 480px the brand now
shows its pin mark only (the wordmark stays its accessible name) and the
field gets 158px. The search, GitHub star, map mode and map favourite
icons were emoji, which draw as nothing without a colour emoji font (every
Windows browser): they are inline SVG or canvas shapes now. Chromium's
built-in clear button no longer doubles the field's own ✕.
A reload always landed on the map and the address stayed `/`, so a city
page, a country page or a tab could not survive a reload or be linked, and
the browser's Forward did nothing. The tab and the open place page now
live in the hash (`#/places/country/FR`), restored before the first
render; each navigation pushes an entry carrying its position, so Back
keeps its rules (dialog first, sub-view, the app's own history, never
quitting at home), walks the screens from before a reload, and Forward
re-opens what Back left. The trip composer, an unsaved form, stays out.
The reminder to export a backup, the one thing between a lost phone and
lost travels, only existed 2,226 px down Settings. Once a backup is due
(data and none for 30 days, not snoozed) the same reminder, with the same
wording, Back up now and Later, now heads Places as one slim line; the
Settings one is the same component.
The Twemoji flag glyph fills about 85% of the em a colour-emoji flag
fills, so every flag in a list came out a size smaller than the layout
was drawn for. The font face now carries size-adjust: 118%, which also
covers canvases, so the map marker goes back to its original font size.
On a 4G phone with a 4x slower CPU nothing painted for about 7.5 s: the
entry imported MapLibre's chunk (its stylesheet was assigned to that
chunk, and the trip composer's route map imported it statically), so
all of MapLibre downloaded and ran before the app started, and the first
render then waited for every reference file while five preloads fought
the bundle for bandwidth. MapLibre now loads with the map, index.html
carries the top bar as a static shell, the first render waits only for
the cities and their regions (the two files still preloaded), and
airports, heritage sites, languages and article names merge in behind it
with the gazetteer event; search, Places and the map refresh on it, and
a reload straight onto Trips, the Journal or a place page waits for them.
Measured on that profile (median of 3): first paint 7.6 s to 1.6 s, app
usable 8.5 s to 4.9 s, map 8.5 s to 7.0 s.
Writing the screen into the address rewrote every load to `#/map`: a
move to the new address lost the query's fragment it carried over, and
a handoff's cleaned-up address gained a hash (moved.spec). The home
screen is the bare address now, a fragment that names no screen counts
as home, and the address is left as it came at load.
Resolutions:

- referenceData.ts: the railway stations (5 MB) join the data that lands
  after the first paint, beside the airports and heritage sites; the first
  screen, the map in any mode, needs only the cities. A station source
  picked in Settings before they land is kept, not overwritten.
- App.tsx: the address and Back keep this branch's history sync; the
  postcard composer, like the trip composer, stays out of the address and
  waits for the late data. The journal long-press joins the SVG star
  imports.
- MapScreen.tsx: the stations segment of the map-mode pill takes a train
  SVG icon, like the other segments.
- MapView.tsx: logged stations sit under your flags with the airports,
  and their chips draw with the flag font stack; main's resize-on-load
  guard stays.
- useUi.ts, route.ts: opening a screen from the address closes the
  postcard composer too; station pages are place pages, so their
  address already opens them (e2e added).
- Tests: the startup test waits for and checks the stations; the new
  map-resize fake gains listImages.
The full unit run ended in three unhandled rejections from mapLoad.spec,
which fail the run even with every test green: the map redraws its flag
markers once the flag font loads, through map.listImages, and the fake
map had no such method.
With the stations loaded after the first paint, a logged station had no
marker until its visits changed: the map resolved it against an empty
station set, and the key guarding the source rebuild could not see the
data land. Reset that key with the others when the reference data
changes, which also clears the markers when Settings switches the
station source to None.
A reload onto the map's Stations, Airports or Monuments mode could show
"Nothing in this view" until the map moved: the list was built when the
map first reported its view, which can come before those datasets land
after the first paint, and it rebuilt only on a view or mode change.
It now rebuilds when they land.
Picking a place from search or Places scrolled the map's list to its row
with a smooth glide even when the system asks for reduced motion; the
map's own camera already honoured that setting. The scroll now jumps
there instead.
When the app's entry script failed to download, the reload message went
below the static shell index.html now paints into #root: a full-height
app frame, so the message sat out of view on a blank-looking page. It
now takes the shell's place.
…paint

The note on the preloads in index.html listed what follows the first
render; the stations joined that list in the merge of main.
The stations joined the reference data that follows the first paint, but
they are its biggest file (5 MB, about 1 MB compressed): fetched with the
rest, their parse landed before the first render on a fast network and
held the app's frame back by 200 to 400 ms on a CPU slowed fourfold. They
are now fetched once the page is idle, and the airports and heritage
sites no longer wait for them.
Fetched on idle, the 1 MB stations download still shared a throttled
connection with MapLibre's code and the map's outlines, and the map drew
about half a second later than before the stations existed. On the map
they are now asked for from the map's load event (or when the map cannot
start); the Stations mode and every other screen, which may read them
and have no map to compete with, ask at once.
@davd-gzl
davd-gzl changed the base branch from audit-fixes to main September 30, 2026 17:24
Backup keeps the shared reminder component, whose two buttons take the titles
the accessibility pass gave the inline version.
@davd-gzl
davd-gzl merged commit 3ddbb99 into main Sep 30, 2026
1 check passed
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