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
+12 -6
View File
@@ -61,6 +61,7 @@ class AdminScreen : public UIScreen {
void openLoginKb() {
kb().begin("", 15); // admin password: same max length as room/repeater login
kb().prompt = "Password";
kb().clearPlaceholders(); // {loc}/{time} are for messages, not a password
_login_kb = true;
}
@@ -69,6 +70,7 @@ class AdminScreen : public UIScreen {
// {} picker to CLI command-name completion (Custom-command row only).
void openValueKb(const char* initial, bool cli_autocomplete) {
kb().begin(initial, 160);
kb().prompt = "Command";
kb().clearPlaceholders(); // a CLI command/value is literal, not a message
if (cli_autocomplete) kb().setPlaceholderRefresh(refreshCmdPlaceholders, nullptr, "Commands:");
_kb_active = true;
@@ -162,6 +164,13 @@ public:
}
}
// A waiting line centred under the header, with the loading dots below it.
int waiting(DisplayDriver& display, const char* what) {
int mid = (display.listStart() + display.height()) / 2;
display.drawTextCentered(display.width() / 2, mid - display.lineStep(), what);
return drawLoadingDots(display, display.width() / 2, mid + display.lineStep());
}
int render(DisplayDriver& display) override {
display.setTextSize(1);
display.setColor(DisplayDriver::LIGHT);
@@ -170,9 +179,8 @@ public:
AdminSession::State st = S().state();
if (st == AdminSession::LOGGING_IN || st == AdminSession::IDLE || st == AdminSession::NEED_PASSWORD) {
display.drawCenteredHeader("ADMIN LOGIN");
display.setCursor(2, display.listStart());
if (st == AdminSession::LOGGING_IN) display.print("Logging in...");
return 500;
if (st != AdminSession::LOGGING_IN) return 500;
return waiting(display, "Logging in");
}
if (_showing_reply) return _reply_view.render(display, S().target().name, S().reply(), false, false);
if (_kb_active) return kb().render(display);
@@ -180,9 +188,7 @@ public:
char title[24];
snprintf(title, sizeof(title), "%.23s", S().target().name);
display.drawCenteredHeader(title);
display.setCursor(2, display.listStart());
display.print(S().fetching() ? "Fetching..." : "Waiting for reply...");
return 500;
return waiting(display, S().fetching() ? "Fetching" : "Waiting for reply");
}
tabbar::draw(display, admin::TAB_LABELS, admin::TAB_COUNT, _tab);