better mobile interface

alphons <alphons@heijden.com> 5 Aug 2026, 21:30
7930a2061ef31ed79795ce9e895dd69bd74dd41d
9 files changed
  • MailSharp.MailClient/Localization/en.json
  • MailSharp.MailClient/Localization/nl.json
  • MailSharp.MailClient/Views/Contacts/Index.cshtml
  • MailSharp.MailClient/Views/Mail/Index.cshtml
  • MailSharp.MailClient/Views/Maintenance/Index.cshtml
  • MailSharp.MailClient/Views/Settings/Index.cshtml
  • MailSharp.MailClient/Views/Shared/_Topbar.cshtml
  • MailSharp.MailClient/wwwroot/css/site.css
  • MailSharp.MailClient/wwwroot/js/common.js
diff --git a/MailSharp.MailClient/Localization/en.json b/MailSharp.MailClient/Localization/en.json
index 3bd660d..253e207 100644
--- a/MailSharp.MailClient/Localization/en.json
+++ b/MailSharp.MailClient/Localization/en.json
@@ -14,6 +14,7 @@
"sidebar_new_message": "✎ New message",
"topbar_contacts": "Contacts",
"topbar_settings": "Settings",
+ "topbar_maintenance": "Manage",
"login_page_title": "Log in",
"login_subtitle": "Log in with your email account",
diff --git a/MailSharp.MailClient/Localization/nl.json b/MailSharp.MailClient/Localization/nl.json
index 393913e..9e12a20 100644
--- a/MailSharp.MailClient/Localization/nl.json
+++ b/MailSharp.MailClient/Localization/nl.json
@@ -14,6 +14,7 @@
"sidebar_new_message": "✎ Nieuw bericht",
"topbar_contacts": "Contacten",
"topbar_settings": "Instellingen",
+ "topbar_maintenance": "Beheer",
"login_page_title": "Inloggen",
"login_subtitle": "Log in met je e-mailaccount",
diff --git a/MailSharp.MailClient/Views/Contacts/Index.cshtml b/MailSharp.MailClient/Views/Contacts/Index.cshtml
index ecd31dd..92cc503 100644
--- a/MailSharp.MailClient/Views/Contacts/Index.cshtml
+++ b/MailSharp.MailClient/Views/Contacts/Index.cshtml
@@ -1,18 +1,10 @@
@{
ViewData["Title"] = "Contacten";
+ ViewData["HasSidebar"] = true;
+ ViewData["ActiveNav"] = "contacts";
}
<div class="app">
- <div class="topbar">
- <button class="hamburger" id="sidebarToggle" aria-label="Menu">&#9776;</button>
- <span class="brand">MailSharp</span>
- <a class="account-email" id="accountEmail" asp-controller="Mail" asp-action="Index"></a>
- <a class="btn btn-text active" asp-controller="Contacts" asp-action="Index" data-i18n="topbar_contacts"></a>
- <div class="spacer"></div>
- <select class="lang-select" id="langSelect"></select>
- <a class="btn btn-text" asp-controller="Maintenance" asp-action="Index" id="maintenanceLink" style="display:none">Beheer</a>
- <a class="btn btn-text" asp-controller="Settings" asp-action="Index" data-i18n="topbar_settings"></a>
- <a class="btn" href="#" id="logoutBtn" data-i18n="topbar_logout"></a>
- </div>
+ @await Html.PartialAsync("_Topbar")
<div class="app-body">
<div class="sidebar" id="sidebar">
<input type="text" id="contactSearch" data-i18n-placeholder="contacts_search" style="margin-bottom:10px;" />
diff --git a/MailSharp.MailClient/Views/Mail/Index.cshtml b/MailSharp.MailClient/Views/Mail/Index.cshtml
index a307da6..cbcd22b 100644
--- a/MailSharp.MailClient/Views/Mail/Index.cshtml
+++ b/MailSharp.MailClient/Views/Mail/Index.cshtml
@@ -1,18 +1,9 @@
@{
ViewData["Title"] = "Mail";
+ ViewData["HasSidebar"] = true;
}
<div class="app">
- <div class="topbar">
- <button class="hamburger" id="sidebarToggle" aria-label="Menu">&#9776;</button>
- <span class="brand">MailSharp</span>
- <a class="account-email" id="accountEmail" asp-controller="Mail" asp-action="Index"></a>
- <a class="btn btn-text" asp-controller="Contacts" asp-action="Index" data-i18n="topbar_contacts"></a>
- <div class="spacer"></div>
- <select class="lang-select" id="langSelect"></select>
- <a class="btn btn-text" asp-controller="Maintenance" asp-action="Index" id="maintenanceLink" style="display:none">Beheer</a>
- <a class="btn btn-text" asp-controller="Settings" asp-action="Index" data-i18n="topbar_settings"></a>
- <a class="btn" href="#" id="logoutBtn" data-i18n="topbar_logout"></a>
- </div>
+ @await Html.PartialAsync("_Topbar")
<div class="app-body">
<div class="sidebar" id="sidebar">
<a class="btn btn-primary compose-btn" asp-controller="Mail" asp-action="Compose" data-i18n="sidebar_new_message"></a>
diff --git a/MailSharp.MailClient/Views/Maintenance/Index.cshtml b/MailSharp.MailClient/Views/Maintenance/Index.cshtml
index e1d32ee..a58d35e 100644
--- a/MailSharp.MailClient/Views/Maintenance/Index.cshtml
+++ b/MailSharp.MailClient/Views/Maintenance/Index.cshtml
@@ -1,17 +1,9 @@
@{
ViewData["Title"] = "Beheer";
+ ViewData["ActiveNav"] = "maintenance";
}
<div class="app">
- <div class="topbar">
- <span class="brand">MailSharp</span>
- <a class="account-email" id="accountEmail" asp-controller="Mail" asp-action="Index"></a>
- <a class="btn btn-text" asp-controller="Contacts" asp-action="Index" data-i18n="topbar_contacts"></a>
- <div class="spacer"></div>
- <select class="lang-select" id="langSelect"></select>
- <a class="btn btn-text active" asp-controller="Maintenance" asp-action="Index">Beheer</a>
- <a class="btn btn-text" asp-controller="Settings" asp-action="Index" data-i18n="topbar_settings"></a>
- <a class="btn" href="#" id="logoutBtn" data-i18n="topbar_logout"></a>
- </div>
+ @await Html.PartialAsync("_Topbar")
<div class="subheader" id="maintenanceTabs">
<a href="#" class="subheader-tab active" data-tab="status">Achtergrondprocessen</a>
<a href="#" class="subheader-tab" data-tab="metrics">Berichten per account</a>
diff --git a/MailSharp.MailClient/Views/Settings/Index.cshtml b/MailSharp.MailClient/Views/Settings/Index.cshtml
index 9a083ec..1cf3997 100644
--- a/MailSharp.MailClient/Views/Settings/Index.cshtml
+++ b/MailSharp.MailClient/Views/Settings/Index.cshtml
@@ -1,17 +1,9 @@
@{
ViewData["Title"] = "Instellingen";
+ ViewData["ActiveNav"] = "settings";
}
<div class="app">
- <div class="topbar">
- <span class="brand">MailSharp</span>
- <a class="account-email" id="accountEmail" asp-controller="Mail" asp-action="Index"></a>
- <a class="btn btn-text" asp-controller="Contacts" asp-action="Index" data-i18n="topbar_contacts"></a>
- <div class="spacer"></div>
- <select class="lang-select" id="langSelect"></select>
- <a class="btn btn-text" asp-controller="Maintenance" asp-action="Index" id="maintenanceLink" style="display:none">Beheer</a>
- <a class="btn btn-text active" asp-controller="Settings" asp-action="Index" data-i18n="topbar_settings"></a>
- <a class="btn" href="#" id="logoutBtn" data-i18n="topbar_logout"></a>
- </div>
+ @await Html.PartialAsync("_Topbar")
<div class="subheader" id="settingsTabs">
<a href="#" class="subheader-tab active" data-tab="general" data-i18n="settings_tab_general"></a>
<a href="#" class="subheader-tab" data-tab="folders" data-i18n="settings_tab_folders"></a>
diff --git a/MailSharp.MailClient/Views/Shared/_Topbar.cshtml b/MailSharp.MailClient/Views/Shared/_Topbar.cshtml
new file mode 100644
index 0000000..11e1971
--- /dev/null
+++ b/MailSharp.MailClient/Views/Shared/_Topbar.cshtml
@@ -0,0 +1,35 @@
+@{
+ // ViewData["HasSidebar"] (bool): renders the hamburger that toggles this page's own #sidebar
+ // (folder list on Mail, contact list on Contacts) - Settings/Maintenance have no sidebar of
+ // their own, so they don't get this button.
+ // ViewData["ActiveNav"] (string): "contacts" | "settings" | "maintenance" | null - which link
+ // (if any) gets the "active" styling for the current page.
+ var hasSidebar = ViewData["HasSidebar"] as bool? ?? false;
+ var activeNav = ViewData["ActiveNav"] as string;
+}
+<div class="topbar">
+ @if (hasSidebar)
+ {
+ <button class="hamburger" id="sidebarToggle" aria-label="Menu">&#9776;</button>
+ }
+ <span class="brand">MailSharp</span>
+ <a class="account-email" id="accountEmail" asp-controller="Mail" asp-action="Index"></a>
+ <div class="spacer"></div>
+ <!--
+ Everything past this point used to just sit directly in the topbar's own flex row - on a
+ phone-width screen there's simply too much here (Contacten, taal, Beheer, Instellingen,
+ Uitloggen) to fit without either shrinking it illegibly or scrolling the bar sideways
+ (both tried, both bad). Wrapping it in its own collapsible panel, opened via the "meer"
+ button, means the topbar itself never has to fit more than brand + hamburger + one toggle
+ button on mobile - see common.js for the toggle behavior and site.css's mobile breakpoint
+ for what turns this from an inline row into a dropdown panel.
+ -->
+ <div class="topbar-links" id="topbarLinks">
+ <a class="btn btn-text @(activeNav == "contacts" ? "active" : "")" asp-controller="Contacts" asp-action="Index" data-i18n="topbar_contacts"></a>
+ <select class="lang-select" id="langSelect"></select>
+ <a class="btn btn-text @(activeNav == "maintenance" ? "active" : "")" asp-controller="Maintenance" asp-action="Index" id="maintenanceLink" style="display:none" data-i18n="topbar_maintenance"></a>
+ <a class="btn btn-text @(activeNav == "settings" ? "active" : "")" asp-controller="Settings" asp-action="Index" data-i18n="topbar_settings"></a>
+ <a class="btn" href="#" id="logoutBtn" data-i18n="topbar_logout"></a>
+ </div>
+ <button class="topbar-menu-toggle" id="topbarMenuToggle" aria-label="Menu" aria-expanded="false">&#8942;</button>
+</div>
diff --git a/MailSharp.MailClient/wwwroot/css/site.css b/MailSharp.MailClient/wwwroot/css/site.css
index 9f86b1a..a27eb0e 100644
--- a/MailSharp.MailClient/wwwroot/css/site.css
+++ b/MailSharp.MailClient/wwwroot/css/site.css
@@ -22,6 +22,10 @@ html, body {
color: var(--text);
font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 15px;
+ /* Safety net so nothing (the topbar's own flex row not shrinking, an overlong subject line,
+ whatever) can force the whole page wider than the viewport on mobile - anything that still
+ doesn't fit scrolls inside its own container instead (see .topbar's mobile rule below). */
+ overflow-x: hidden;
}
a { color: var(--primary); text-decoration: none; }
@@ -142,6 +146,7 @@ label { font-size: 13px; color: var(--text-muted); display: block; margin-bottom
}
.topbar {
+ position: relative;
display: flex;
align-items: center;
gap: 10px;
@@ -155,6 +160,12 @@ label { font-size: 13px; color: var(--text-muted); display: block; margin-bottom
.account-email { color: var(--text-muted); font-size: 14px; text-decoration: none; }
.account-email:hover { color: var(--text); text-decoration: underline; }
.lang-select { width: auto; flex-shrink: 0; }
+/* Desktop: just another row of inline items, same as before. Mobile (see the media query below)
+ turns this into a collapsible dropdown panel instead - see _Topbar.cshtml for why it's grouped
+ into its own container at all. */
+.topbar-links { display: flex; align-items: center; gap: 10px; }
+.topbar-menu-toggle { display: none; border: none; background: transparent; font-size: 20px; padding: 4px 10px; cursor: pointer; color: var(--text); border-radius: 6px; }
+.topbar-menu-toggle:hover { background: var(--accent-bg); }
.hamburger {
display: none;
border: none;
@@ -480,6 +491,31 @@ body.col-resizing { cursor: col-resize; user-select: none; }
@media (max-width: 860px) {
.hamburger { display: inline-block; }
.col-resizer { display: none; }
+ /* Contacten/taal/Beheer/Instellingen/Uitloggen move into a dropdown behind the "⋮" button
+ instead of trying to fit inline - the topbar itself only ever has to fit brand + hamburger +
+ this one toggle button, so it no longer needs to scroll or shrink anything illegibly. Account
+ email still drops entirely - redundant (the user knows who they are) and there's no room for
+ it even in the collapsed bar. */
+ .account-email { display: none; }
+ .topbar-menu-toggle { display: inline-block; }
+ .topbar-links {
+ display: none;
+ position: absolute;
+ top: 52px;
+ right: 10px;
+ flex-direction: column;
+ align-items: stretch;
+ gap: 4px;
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ padding: 8px;
+ min-width: 190px;
+ z-index: 30;
+ }
+ .topbar-links.open { display: flex; }
+ .topbar-links .lang-select { width: 100%; }
.sidebar {
position: fixed;
top: 53px;
diff --git a/MailSharp.MailClient/wwwroot/js/common.js b/MailSharp.MailClient/wwwroot/js/common.js
index 5aa64c4..f19d92b 100644
--- a/MailSharp.MailClient/wwwroot/js/common.js
+++ b/MailSharp.MailClient/wwwroot/js/common.js
@@ -243,6 +243,40 @@ window.MailSharp = (function ()
if (btn) btn.addEventListener("click", cancelActiveRequests);
});
+ // Mobile "⋮" toggle for the topbar's collapsed link panel (see _Topbar.cshtml/site.css) - shared
+ // across every page's topbar, not just Mail's, since Settings/Maintenance have this same button
+ // now too. Closes on an outside click or Escape, same as any other dropdown/menu.
+ document.addEventListener("DOMContentLoaded", function ()
+ {
+ var toggle = document.getElementById("topbarMenuToggle");
+ var links = document.getElementById("topbarLinks");
+ if (!toggle || !links) return;
+
+ function closeMenu()
+ {
+ links.classList.remove("open");
+ toggle.setAttribute("aria-expanded", "false");
+ }
+
+ toggle.addEventListener("click", function (e)
+ {
+ e.stopPropagation();
+ var willOpen = !links.classList.contains("open");
+ links.classList.toggle("open", willOpen);
+ toggle.setAttribute("aria-expanded", willOpen ? "true" : "false");
+ });
+
+ document.addEventListener("click", function (e)
+ {
+ if (links.classList.contains("open") && !links.contains(e.target) && e.target !== toggle) closeMenu();
+ });
+
+ document.addEventListener("keydown", function (e)
+ {
+ if (e.key === "Escape") closeMenu();
+ });
+ });
+
var indexingEventSource = null;
// entry.total is only known once a folder's full reindex has started scanning (see