diff --git a/brand/README.md b/brand/README.md index 2a3feb67d..7838b5ee0 100644 --- a/brand/README.md +++ b/brand/README.md @@ -21,13 +21,16 @@ are cached for five minutes, so runtime logo changes can take that long to appea ## Runtime browser favicons -In **Settings → Branding**, upload **Favicon for light mode** and **Favicon for -dark mode** independently. Each accepts SVG, PNG, or ICO up to 100 KiB. Selection +In **Settings → Branding**, upload three independent icons: **Default favicon**, +**Light mode favicon**, and **Dark mode favicon**. Each accepts SVG, PNG, or ICO +up to 100 KiB. Use an icon with a background for the default and transparent +light/dark overrides if desired; uploads are served unchanged. Override selection follows the browser/OS `prefers-color-scheme`, not the app's theme setting, and changes without reloading the page. -The branding keys are `faviconLight` and `faviconDark`. A missing variant uses -the existing `favicon` (the **Favicon (fallback)** slot), then the bundled mark. +The branding keys are `favicon` (default), `faviconLight`, and `faviconDark`. +Clients without theme selection and schemes without an override use the default; +if no default is uploaded, they use the bundled mark. The same keys travel with branding ZIP exports/imports and can be set through the branding CLI or XRPC. No custom frontend build is needed for uploads. diff --git a/js/app/components/settings/branding-admin.tsx b/js/app/components/settings/branding-admin.tsx index c3c3af72d..2276bb135 100644 --- a/js/app/components/settings/branding-admin.tsx +++ b/js/app/components/settings/branding-admin.tsx @@ -2402,7 +2402,11 @@ export function BrandingAdmin() { {t(label)} {asset?.data && ( - ``; const ICONS: Record = { - favicon: svg("#808080"), - faviconLight: svg("#ffd000"), - faviconDark: svg("#2040ff"), + favicon: ``, + faviconLight: ``, + faviconDark: ``, }; type Icon = { type: string; body: string }; @@ -141,7 +139,14 @@ test("branding-favicons: light and dark favicons follow the color scheme", async .toBe("image/png"); const bundled = await shownIcon(page, "dom"); - for (const key of Object.keys(ICONS)) await uploadIcon(page, key); + // The background-backed default works in either scheme on its own. + await uploadIcon(page, "favicon"); + await expectShown(page, "light", icon("favicon")); + await expectShown(page, "dark", icon("favicon")); + + // Transparent variants override it independently without altering the files. + for (const key of ["faviconLight", "faviconDark"]) + await uploadIcon(page, key); // Each scheme gets its own icon, live: no reload between the switches. await expectShown(page, "light", icon("faviconLight")); diff --git a/js/i18n/locales/en-US/settings.ftl b/js/i18n/locales/en-US/settings.ftl index d02dd21d5..e6edc3db6 100644 --- a/js/i18n/locales/en-US/settings.ftl +++ b/js/i18n/locales/en-US/settings.ftl @@ -307,10 +307,11 @@ branding-accent-color = Accent Color (secondary surfaces, highlights) branding-accent-color-placeholder = #8b5cf6 branding-main-logo = Main Logo branding-main-logo-description = SVG, PNG, or JPEG (max 500KB) -branding-favicon = Favicon (fallback) -branding-favicon-light = Favicon for light mode -branding-favicon-dark = Favicon for dark mode -branding-favicon-description = SVG, PNG, or ICO (max 100KB). Light/dark icons follow the browser or OS appearance, not the app theme. Missing variants use the fallback favicon, or the bundled icon if none is uploaded. +branding-favicon = Default favicon +branding-favicon-light = Light mode favicon +branding-favicon-dark = Dark mode favicon +branding-favicon-description = SVG, PNG, or ICO (max 100KB). Used when an override is absent or the client does not support theme selection. An icon with a background works well as the default. +branding-favicon-override-description = SVG, PNG, or ICO (max 100KB). Overrides the default for the browser or OS appearance, not the app theme. Transparent backgrounds are supported. branding-sidebar-bg = Sidebar Background Image branding-sidebar-bg-description = SVG, PNG, or JPEG (max 500kb) - appears aligned to bottom of sidebar, full width. Upload an image with opacity for best results, as there is not currently a separate opacity option. branding-current = Current: { $value } diff --git a/js/i18n/public/locales/en-US/settings.json b/js/i18n/public/locales/en-US/settings.json index 189780e36..e213dca21 100644 --- a/js/i18n/public/locales/en-US/settings.json +++ b/js/i18n/public/locales/en-US/settings.json @@ -247,10 +247,11 @@ "branding-accent-color-placeholder": "#8b5cf6", "branding-main-logo": "Main Logo", "branding-main-logo-description": "SVG, PNG, or JPEG (max 500KB)", - "branding-favicon": "Favicon (fallback)", - "branding-favicon-light": "Favicon for light mode", - "branding-favicon-dark": "Favicon for dark mode", - "branding-favicon-description": "SVG, PNG, or ICO (max 100KB). Light/dark icons follow the browser or OS appearance, not the app theme. Missing variants use the fallback favicon, or the bundled icon if none is uploaded.", + "branding-favicon": "Default favicon", + "branding-favicon-light": "Light mode favicon", + "branding-favicon-dark": "Dark mode favicon", + "branding-favicon-description": "SVG, PNG, or ICO (max 100KB). Used when an override is absent or the client does not support theme selection. An icon with a background works well as the default.", + "branding-favicon-override-description": "SVG, PNG, or ICO (max 100KB). Overrides the default for the browser or OS appearance, not the app theme. Transparent backgrounds are supported.", "branding-sidebar-bg": "Sidebar Background Image", "branding-sidebar-bg-description": "SVG, PNG, or JPEG (max 500kb) - appears aligned to bottom of sidebar, full width. Upload an image with opacity for best results, as there is not currently a separate opacity option.", "branding-current": "Current: {$value}", diff --git a/js/web/public/locales/en-US/settings.json b/js/web/public/locales/en-US/settings.json index 189780e36..e213dca21 100644 --- a/js/web/public/locales/en-US/settings.json +++ b/js/web/public/locales/en-US/settings.json @@ -247,10 +247,11 @@ "branding-accent-color-placeholder": "#8b5cf6", "branding-main-logo": "Main Logo", "branding-main-logo-description": "SVG, PNG, or JPEG (max 500KB)", - "branding-favicon": "Favicon (fallback)", - "branding-favicon-light": "Favicon for light mode", - "branding-favicon-dark": "Favicon for dark mode", - "branding-favicon-description": "SVG, PNG, or ICO (max 100KB). Light/dark icons follow the browser or OS appearance, not the app theme. Missing variants use the fallback favicon, or the bundled icon if none is uploaded.", + "branding-favicon": "Default favicon", + "branding-favicon-light": "Light mode favicon", + "branding-favicon-dark": "Dark mode favicon", + "branding-favicon-description": "SVG, PNG, or ICO (max 100KB). Used when an override is absent or the client does not support theme selection. An icon with a background works well as the default.", + "branding-favicon-override-description": "SVG, PNG, or ICO (max 100KB). Overrides the default for the browser or OS appearance, not the app theme. Transparent backgrounds are supported.", "branding-sidebar-bg": "Sidebar Background Image", "branding-sidebar-bg-description": "SVG, PNG, or JPEG (max 500kb) - appears aligned to bottom of sidebar, full width. Upload an image with opacity for best results, as there is not currently a separate opacity option.", "branding-current": "Current: {$value}",