feat(l1): L2 splash, hovering Home icons, soft selections, two-line DM bubbles

- Splash: the L2 wordmark and lettering, shared through ui-core/Lettering.h,
  rise in with loading dots under the version line (static frame on e-ink).
- Home: the header shows the time once the clock is set; icon pages carry
  only their title. One 32 px icon family (3 px strokes) bobs 2 px over a
  dithered blob shadow that shrinks as it rises; the unread badge rides on
  the Messages bubble; Bluetooth off is the icon at half density.
- Soft corners on every selection, the page pill, tabs and popups;
  dotted scrollbar track with a rounded thumb; the tab bar lays out whole
  tabs and shows edge arrows only in free space.
- DM bubbles drop the sender row and wrap to a second line, with the
  delivery mark and age closing it.
- Keyboards name what is typed (channel, contact, Password, Scope...)
  and show characters left near the limit; PIN entry has its own preview.
- Loading dots for waits (login, scan, GPS) and signal bars in place of
  raw RSSI in Nearby.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-10-01 22:59:44 +02:00
co-authored by Claude Opus 5.5
parent 1580de53f5
commit 608231ed6b
18 changed files with 877 additions and 500 deletions
+13 -5
View File
@@ -497,8 +497,10 @@ class NearbyScreen : public UIScreen, protected NearbyModel {
display.setCursor(2, hdr + step); display.print(buf);
snprintf(buf, sizeof(buf), "SNR: %.1f dB", e.snr_x4 / 4.0f);
display.setCursor(2, hdr + step * 2); display.print(buf);
drawSignalBars(display, display.width() - 2, hdr + step * 2, e.snr_x4); // how we hear them
snprintf(buf, sizeof(buf), "Rem: %.1f dB", e.remote_snr_x4 / 4.0f);
display.setCursor(2, hdr + step * 3); display.print(buf);
drawSignalBars(display, display.width() - 2, hdr + step * 3, e.remote_snr_x4); // how they hear us
display.setCursor(2, hdr + step * 4);
display.print(e.is_known ? "Status: known" : "Status: new");
}
@@ -631,10 +633,15 @@ public:
char empty[24];
if (_source == SRC_SCAN) {
const char* msg;
if (_scanning) msg = "No replies yet"; // fits 128 px; the header already says SCANNING
if (_scanning) { // the header already says SCANNING: just show it's listening
display.drawTextCentered(display.width() / 2, display.height() / 2 - display.lineStep() / 2, "No replies yet");
int r = drawLoadingDots(display, display.width() / 2, display.height() / 2 + display.lineStep() + 2);
if (mq_delay <= 0 || r < mq_delay) mq_delay = r;
msg = nullptr;
}
else if (flt) { snprintf(empty, sizeof(empty), "No %s nodes", flt); msg = empty; }
else msg = "No nodes found";
display.drawTextCentered(display.width() / 2, display.height() / 2, msg);
if (msg) display.drawTextCentered(display.width() / 2, display.height() / 2, msg);
} else {
const char* hint;
if (_pick_admin_target && !flt) { snprintf(empty, sizeof(empty), "Nothing to admin"); hint = "Repeaters & rooms"; }
@@ -675,8 +682,9 @@ public:
display.setColor(sel ? DisplayDriver::DARK : DisplayDriver::LIGHT);
char right[10];
if (_source == SRC_SCAN) {
snprintf(right, sizeof(right), "%d", (int)e.rssi);
if (_source == SRC_SCAN) { // live scan: how well we hear it, as bars
drawSignalBars(display, display.width() - reserve - 2, y, e.snr_x4);
right[0] = '\0';
} else if (_sort == SORT_TIME) {
geo::fmtAgeShort(right, sizeof(right), rtc_clock.getCurrentTime(), e.lastmod);
if (!right[0]) snprintf(right, sizeof(right), "?"); // unknown / RTC not synced
@@ -684,7 +692,7 @@ public:
if (e.dist_km >= 0.0f) geo::fmtDist(right, sizeof(right), e.dist_km, useImperial());
else strncpy(right, "?GPS", sizeof(right));
}
display.drawTextRightAlign(display.width() - reserve - 2, y, right);
if (right[0]) display.drawTextRightAlign(display.width() - reserve - 2, y, right);
});
}