feat(theme): ship exactly 2 themes (SRC default + VNClagoon); rebrand user-facing VNCmail+

Product decision 2026-08-05: exactly SRC (default) and VNClagoon ship as
selectable themes. Everything else (Qui, Nord, Catppuccin, Solarized,
Roundcube Elastic, Aurora Glass) is hidden via ThemePolicy.disabledBuiltinThemes
rather than deleted - cheap to re-enable later, zero code lost. Also removed
the hardcoded "Default/Bulwark" theme card from the Settings > Themes grid
(product wants exactly 2 theme choices, not 3).

Separately, found and fixed real "still says Bulwark" branding gaps:
- All 24 locale files: "Bulwark"/"Bulwark Mail"/"Bulwark Webmail" ->
  "VNCmail+" in every user-facing string (verified via the translations
  test, which only checks structural key parity across locales, not
  content - a straight string swap is safe against it. 48/48 still pass).
- PWA manifest fallback app name, package.json description/author.
- Demo mode fixtures: the "Welcome to Bulwark Mail!" email and identity
  signature a first-time demo user actually sees.
- The demo empty-state's logo was hardcoded to a literal Bulwark SVG file
  regardless of active theme - a real bug, not just stale text, since
  lib/theme-logo.ts's resolveThemeLogo() already exists and is already used
  correctly by the login page and nav rail for exactly this (theme-aware
  SRC mark / VNClagoon wordmark). Wired the same helper in here instead of
  a hardcoded path.

NOT touched: internal code comments referencing "Bulwark" as historical/
attribution context (e.g. design-rationale comments explaining why a value
differs from Bulwark's original) - those are harmless and a full-repo sweep
of every comment wasn't the ask.

Verified: typecheck clean, lint clean, translations test 48/48 passing.
This commit is contained in:
Bernd Rodler
2026-08-05 23:43:19 +02:00
parent 3f99a2fc9e
commit bde9f14832
31 changed files with 416 additions and 404 deletions
+12 -4
View File
@@ -9,6 +9,7 @@ import { EMAIL_IFRAME_SANITIZE_CONFIG, applyNewTabToAnchor, blockExternalResourc
import { hasMeaningfulHtmlBody } from "@/lib/signature-utils";
import { collapsePlainTextQuotes, setupQuoteCollapse } from "@/lib/quote-collapse";
import { withBasePath } from "@/lib/browser-navigation";
import { resolveThemeLogo } from "@/lib/theme-logo";
import { Button } from "@/components/ui/button";
import { Avatar } from "@/components/ui/avatar";
import { formatFileSize, cn, buildMailboxTree, MailboxNode, formatDateTime, generateUUID } from "@/lib/utils";
@@ -766,6 +767,8 @@ export function EmailViewer({
return new Date(time).toISOString();
}, [client, t, tComposer]);
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
const activeThemeId = useThemeStore((state) => state.activeThemeId);
const installedThemes = useThemeStore((state) => state.installedThemes);
const { startTour } = useTour();
const isEmbedded = useIsEmbedded();
const [showFullHeaders, setShowFullHeaders] = useState(false);
@@ -2729,15 +2732,20 @@ export function EmailViewer({
if (!email) {
if (isDemoMode) {
const logoSrc = withBasePath(resolvedTheme === 'dark'
? '/branding/Bulwark_Logo_with_Lettering_White_and_Color.svg'
: '/branding/Bulwark_Logo_with_Lettering_Dark_Color.svg');
// Same resolution as navigation-rail.tsx/login: active theme's own
// brand logo (SRC mark / VNClagoon wordmark), falling back to the SRC
// mark rather than a hardcoded brand image - this demo empty state has
// no admin-override concept of its own, so there's no global override
// to check here.
const logoSrc = withBasePath(
resolveThemeLogo(installedThemes, activeThemeId, resolvedTheme === 'dark', '/branding/SRC_Symbol.png', '/branding/SRC_Symbol.png'),
);
return (
<div className={cn("flex-1 flex flex-col items-center justify-center bg-gradient-to-br from-muted/30 to-muted/50", className)}>
<div className="text-center p-8 max-w-md">
<img
src={logoSrc}
alt="Bulwark Mail"
alt="VNCmail+"
className="h-12 mx-auto mb-6"
/>
<h3 className="text-xl font-semibold text-foreground mb-3">{tDemoWelcome('title')}</h3>
+5 -15
View File
@@ -72,21 +72,11 @@ export function ThemesSettings() {
{/* Theme Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{/* Default theme card */}
<ThemeCard
name="Default"
author="Bulwark"
isDefaultTheme
variants={['light', 'dark']}
isDark={isDark}
isActive={activeThemeId === null}
isBuiltIn
isDefault={!themePolicy.defaultThemeId}
disabled={Boolean(forcedThemeId)}
onActivate={() => handleActivate(null)}
/>
{/* Installed themes */}
{/* No "Default/Bulwark" card: product decision 2026-08-05 ships exactly
two themes (SRC default, VNClagoon) - see DEFAULT_THEME_POLICY in
lib/admin/types.ts. The underlying activateTheme(null) capability
stays reachable programmatically (e.g. an admin clearing
defaultThemeId), just not offered as a selectable card here. */}
{visibleThemes.map(theme => {
const isForceEnabled = theme.id === forcedThemeId || theme.forceEnabled || isThemeForceEnabled(theme.id);
return (