The logo beside the add-feed button, with the version on hover (#60)

The logo sat at the top of the feed list with "iPX" written beside it,
and the page showed the version nowhere. It is now in the top bar just
before the feed buttons, alone, and its tooltip names the app and its
version.

The version is filled in by the server as it sends the page, not by
build.mjs: build.rs reruns only when web/ or package-lock.json changes,
so a release that bumped only Cargo.toml would have kept the page naming
the one before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-28 17:45:35 +00:00
parent 70e0341348
commit 4769a2ebe1
4 changed files with 16 additions and 10 deletions

View File

@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- The logo's needle is a brighter, redder orange that stands apart from the blue bars. - The logo's needle is a brighter, redder orange that stands apart from the blue bars.
- The logo is in the top bar beside the add-feed button, in place of the name at the top of the
feed list. Hovering it shows iPX's version.
### Fixed ### Fixed

View File

@@ -610,6 +610,9 @@ fn constant_time_eq(a: &str, b: &str) -> bool {
// quotes, so ADMIN_LINK and HTML_TAG are spelled the way the minifier leaves them. // quotes, so ADMIN_LINK and HTML_TAG are spelled the way the minifier leaves them.
const INDEX: &str = include_str!(concat!(env!("OUT_DIR"), "/index.html")); const INDEX: &str = include_str!(concat!(env!("OUT_DIR"), "/index.html"));
const ADMIN_LINK: &str = "<a id=admin "; const ADMIN_LINK: &str = "<a id=admin ";
// The logo's tooltip. Filled in here, not by build.mjs, because build.rs does not rerun when only
// Cargo.toml's version changes, and the page would go on naming the last release.
const VERSION_SLOT: &str = "iPX {version}";
/// The page, with the log button left out for anyone but an admin. Hiding it from the page's /// The page, with the log button left out for anyone but an admin. Hiding it from the page's
/// script instead showed it for a moment on every load, until /api/me answered. /// script instead showed it for a moment on every load, until /api/me answered.
@@ -624,7 +627,7 @@ const HTML_TAG: &str = "<html lang=en>";
/// are an admin. Not hidden for everyone else but left out: hiding it from the page's script /// are an admin. Not hidden for everyone else but left out: hiding it from the page's script
/// showed it for a moment on every load, until /api/me answered (issue #29). /// showed it for a moment on every load, until /api/me answered (issue #29).
fn page_for(admin: bool, theme: (Option<String>, Option<String>)) -> String { fn page_for(admin: bool, theme: (Option<String>, Option<String>)) -> String {
let mut page = with_theme(INDEX, theme); let mut page = with_theme(INDEX, theme).replacen(VERSION_SLOT, concat!("iPX ", env!("CARGO_PKG_VERSION")), 1);
if !admin if !admin
&& let Some(at) = page.find(ADMIN_LINK) && let Some(at) = page.find(ADMIN_LINK)
&& let Some(len) = page[at..].find("</a>") && let Some(len) = page[at..].find("</a>")
@@ -1002,6 +1005,13 @@ mod tests {
assert!(page.contains("id=prefs"), "and only the link: the settings button beside it stays"); assert!(page.contains("id=prefs"), "and only the link: the settings button beside it stays");
} }
#[test]
fn the_logo_names_the_version() {
// If the minifier drifts from VERSION_SLOT, replacen matches nothing and says nothing.
let page = page_for(false, (None, None));
assert!(!page.contains(VERSION_SLOT) && page.contains(concat!("iPX ", env!("CARGO_PKG_VERSION"))));
}
#[test] #[test]
fn the_page_arrives_in_the_theme_the_account_chose() { fn the_page_arrives_in_the_theme_the_account_chose() {
let page = |t: &str, m: &str| page_for(true, (Some(t.into()), Some(m.into()))); let page = |t: &str, m: &str| page_for(true, (Some(t.into()), Some(m.into())));

View File

@@ -493,11 +493,7 @@ a{color:var(--accent)}
background:var(--panel);border-right:1px solid var(--line); background:var(--panel);border-right:1px solid var(--line);
display:flex;flex-direction:column;min-height:0; display:flex;flex-direction:column;min-height:0;
} }
.brand{display:flex;align-items:center;gap:9px;padding:14px 14px 10px} #applogo{width:28px;height:28px;flex:none;border-radius:7px}
.brand .logo{
width:30px;height:30px;flex:none;border-radius:7px;
}
.brand h1{font-size:16px;margin:0;font-weight:650;letter-spacing:-.01em;color:var(--fg);flex:1}
.iconbtn{ .iconbtn{
width:30px;height:30px;border-radius:8px;display:grid;place-items:center; width:30px;height:30px;border-radius:8px;display:grid;place-items:center;
color:var(--dim);flex:none; color:var(--dim);flex:none;
@@ -548,7 +544,7 @@ a{color:var(--accent)}
.who button:hover{color:var(--fg)} .who button:hover{color:var(--fg)}
.sidefoot button{display:inline-flex;align-items:center;justify-content:center;gap:6px} .sidefoot button{display:inline-flex;align-items:center;justify-content:center;gap:6px}
.sidefoot i{font-style:normal;opacity:.75} .sidefoot i{font-style:normal;opacity:.75}
.searchwrap{padding:0 12px 8px} .searchwrap{padding:12px 12px 8px}
input[type=search],input[type=text],input[type=password],input[type=number],select{ input[type=search],input[type=text],input[type=password],input[type=number],select{
width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg); width:100%;background:var(--bg);border:1px solid var(--line);color:var(--fg);
border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px; border-radius:8px;padding:7px 10px;font:inherit;font-size:13.5px;

View File

@@ -14,6 +14,7 @@
<button class="iconbtn" id="burger" title="Feeds" aria-label="Feeds" data-icon="menu"></button> <button class="iconbtn" id="burger" title="Feeds" aria-label="Feeds" data-icon="menu"></button>
<!-- One group per thing acted on, in the order the panes read: feeds, then the selected item. <!-- One group per thing acted on, in the order the panes read: feeds, then the selected item.
A phone hides the item group, which it has no table for. --> A phone hides the item group, which it has no table for. -->
<img class="logo" id="applogo" src="/logo.svg" alt="iPX" title="iPX {version}">
<div class="tgroup"> <div class="tgroup">
<button id="addFeed" title="Add a feed" aria-label="Add a feed" data-icon="plus"></button> <button id="addFeed" title="Add a feed" aria-label="Add a feed" data-icon="plus"></button>
<button id="tbRemove" title="Unsubscribe from this feed" aria-label="Unsubscribe from this feed" data-icon="circleMinus" disabled></button> <button id="tbRemove" title="Unsubscribe from this feed" aria-label="Unsubscribe from this feed" data-icon="circleMinus" disabled></button>
@@ -33,9 +34,6 @@
</header> </header>
<div id="shell"> <div id="shell">
<aside id="sidebar"> <aside id="sidebar">
<div class="brand">
<img class="logo" src="/logo.svg" alt="iPX"><h1>iPX</h1>
</div>
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div> <div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
<div id="feedlist"></div> <div id="feedlist"></div>
<div class="sidefoot"> <div class="sidefoot">