Brand the webmail INBUXA: name, mark and wordmark

INBUXA is a product suite and ihasmail an independent product, so INBUXA's
webmail says INBUXA: the sign-in page, header, page title, installed-app name
and About page. The wordmark is drawn in the current text color. ihasmail's
version and AGPL source line stay as its credit. Two new strings, in all nine
catalogues; three Stalwart-only ones are no longer used.
This commit is contained in:
2026-09-18 15:58:56 -07:00
parent cdd8fabff9
commit 2e668edb51
20 changed files with 92 additions and 30 deletions
+9 -4
View File
@@ -4,6 +4,7 @@ import { BookOpen, Calendar, ChevronsUpDown, FolderOpen, Globe, HelpCircle, LogO
import { useSession } from "@/store/session";
import { withBase } from "@/lib/basePath";
import { DEFAULT_APP_NAME } from "@/lib/brand";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
import { useEffectiveTheme, useSettings } from "@/store/settings";
import { toggleTarget } from "@/lib/palette";
import { useMail } from "@/store/mail";
@@ -147,14 +148,18 @@ export function AppShell({ children }: { children: ReactNode }) {
<MenuIcon size={22} />
</button>
<Link href="/mail" className="brand">
<img src={withBase("/img/logo.png")} alt="" />
<img src={withBase(appName === DEFAULT_APP_NAME ? "/img/inbuxa-mark.png" : "/img/logo.png")} alt="" />
{/* A product name, not a word: translated it is a different product.
Read from the session rather than written here, so a deployment
that set APP_NAME is called what it calls itself -- the document
title has taken it from there all along. */}
<span className="brand-name notranslate" translate="no">
{appName}
</span>
{appName === DEFAULT_APP_NAME ? (
<InbuxaWordmark className="brand-name" height={22} />
) : (
<span className="brand-name notranslate" translate="no">
{appName}
</span>
)}
</Link>
<SearchBar />
<div className="topbar-actions">
+6 -3
View File
@@ -7,6 +7,7 @@ import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version";
import { DEFAULT_APP_NAME } from "@/lib/brand";
import { t } from "@/lib/i18n";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
export function LoginPage() {
const login = useSession((s) => s.login);
@@ -92,10 +93,12 @@ export function LoginPage() {
<div className="login-page">
<form className="login-card" onSubmit={submit}>
<div className="logo">
<img src={withBase("/img/logo.png")} alt="" width={120} height={143} />
<img src={withBase(appName === DEFAULT_APP_NAME ? "/img/inbuxa-mark.png" : "/img/logo.png")} alt="" width={120} height={143} />
{/* A product name, not a word: not translated, and not guessed at
from the page it is on. */}
<h1 className="notranslate" translate="no">{appName}</h1>
from the page it is on. INBUXA's is its wordmark. */}
<h1 className="notranslate" translate="no">
{appName === DEFAULT_APP_NAME ? <InbuxaWordmark height={34} /> : appName}
</h1>
<p className="tagline">{t("Fast, friendly webmail. Your mailbox, your way.")}</p>
</div>
{error && (
+9 -16
View File
@@ -4,6 +4,7 @@ import { DEFAULT_SOURCE_URL } from "@/lib/source";
import { APP_VERSION } from "@/lib/version";
import { withBase } from "@/lib/basePath";
import { t, tNode } from "@/lib/i18n";
import { InbuxaWordmark } from "@/ui/InbuxaWordmark";
export function AboutSettings() {
const session = useSession((s) => s.session);
@@ -12,13 +13,16 @@ export function AboutSettings() {
const sourceUrl = session?.ihasmail?.sourceUrl ?? DEFAULT_SOURCE_URL;
return (
<div>
<h1>{t("About ihasmail")}</h1>
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <a href="https://stalw.art" target="_blank" rel="noreferrer">{t("Stalwart Mail Server")}</a> })}</p>
{/* ihasmail-inbuxa: INBUXA's webmail, built on ihasmail, whose version
and source stay here as its credit and the AGPL's offer. */}
<h1>{t("About INBUXA webmail")}</h1>
<p className="lead">{tNode("A fast, friendly, open-source webmail for {server}, built on JMAP.", { server: <span className="notranslate" translate="no">INBUXA</span> })}</p>
<div className="row" style={{ gap: 16, alignItems: "center", marginBottom: 16 }}>
<img src={withBase("/img/logo.png")} alt={t("ihasmail")} width={96} />
<img src={withBase("/img/inbuxa-mark.png")} alt="" width={80} />
<div>
<InbuxaWordmark height={26} />
{/* A product name and a version string: neither is a word to translate. */}
<div style={{ fontWeight: 700, fontSize: "1.2em" }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div>
<div style={{ fontWeight: 700 }} className="notranslate" translate="no">ihasmail v{APP_VERSION}</div>
<div className="hint">{tNode("AGPL-3.0-or-later · {source}", { source: <a href={sourceUrl} target="_blank" rel="noreferrer">{sourceUrl.replace(/^https?:\/\//, "")}</a> })}</div>
</div>
</div>
@@ -26,7 +30,7 @@ export function AboutSettings() {
<table className="sessions-table">
<tbody>
<tr><td>{t("Signed in as")}</td><td>{session?.username}</td></tr>
<tr><td>{t("Stalwart")}</td><td>{describeServer(session?.ihasmail?.server)}</td></tr>
<tr><td>{t("Mail server")}</td><td className="notranslate" translate="no">INBUXA</td></tr>
<tr><td>{t("Accounts")}</td><td>{Object.values(session?.accounts ?? {}).map((a) => a.name).join(", ")}</td></tr>
<tr><td>{t("Max upload")}</td><td>{t("{size} MB", { size: Math.round(client.maxSizeUpload / 1048576) })}</td></tr>
<tr><td>{t("Image privacy proxy")}</td><td>{session?.ihasmail?.imageProxy ? t("enabled") : t("disabled")}</td></tr>
@@ -41,14 +45,3 @@ export function AboutSettings() {
</div>
);
}
/**
* Stalwart deliberately withholds its version from clients (it reports a fixed
* "1.0.0" wherever it publishes one at all), so the edition is all there is to
* show. The generation used to be reported here too, back when ihasmail spoke
* to both 0.15 and 0.16; it requires 0.16 now, so signing in at all is the
* answer to that question.
*/
function describeServer(server: { edition?: string | null } | undefined): string {
return server?.edition ? `0.16 or newer (${server.edition})` : "0.16 or newer";
}