web: fix dark theme and theme switch (#10667)
* base locale off of ak-element Signed-off-by: Jens Langhammer <jens@goauthentik.io> * revert temp theme fixes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix theme switching Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add basic support for theme-different images Signed-off-by: Jens Langhammer <jens@goauthentik.io> * sort outposts in card Signed-off-by: Jens Langhammer <jens@goauthentik.io> * set default theme based on pre-hydrated brand settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * activate global theme before root in shadow dom Signed-off-by: Jens Langhammer <jens@goauthentik.io> * logging Signed-off-by: Jens Langhammer <jens@goauthentik.io> * when using _applyTheme, check media matcher Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
		| @ -56,6 +56,7 @@ export class OutpostStatusChart extends AKChart<SummarizedSyncStatus[]> { | ||||
|             }), | ||||
|         ); | ||||
|         this.centerText = outposts.pagination.count.toString(); | ||||
|         outpostStats.sort((a, b) => a.label.localeCompare(b.label)); | ||||
|         return outpostStats; | ||||
|     } | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants"; | ||||
| import { globalAK } from "@goauthentik/common/global"; | ||||
| import { UIConfig } from "@goauthentik/common/ui/config"; | ||||
| import { adaptCSS } from "@goauthentik/common/utils"; | ||||
| import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet"; | ||||
| @ -16,6 +17,7 @@ type AkInterface = HTMLElement & { | ||||
|     brand?: CurrentBrand; | ||||
|     uiConfig?: UIConfig; | ||||
|     config?: Config; | ||||
|     get activeTheme(): UiThemeEnum | undefined; | ||||
| }; | ||||
|  | ||||
| export const rootInterface = <T extends AkInterface>(): T | undefined => | ||||
| @ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> { | ||||
|     return css; | ||||
| } | ||||
|  | ||||
| const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; | ||||
| export const QUERY_MEDIA_COLOR_LIGHT = "(prefers-color-scheme: light)"; | ||||
|  | ||||
| // Ensure themes are converted to a static instance of CSS Stylesheet, otherwise the | ||||
| // when changing themes we might not remove the correct css stylesheet instance. | ||||
| const _darkTheme = ensureCSSStyleSheet(ThemeDark); | ||||
|  | ||||
| @localized() | ||||
| export class AKElement extends LitElement { | ||||
| @ -77,8 +83,7 @@ export class AKElement extends LitElement { | ||||
|     } | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|         // return rootInterface()?.getTheme() || UiThemeEnum.Automatic; | ||||
|         return rootInterface()?.getTheme() || UiThemeEnum.Light; | ||||
|         return rootInterface()?.getTheme() || UiThemeEnum.Automatic; | ||||
|     } | ||||
|  | ||||
|     fixElementStyles() { | ||||
| @ -91,9 +96,7 @@ export class AKElement extends LitElement { | ||||
|     async _initTheme(root: DocumentOrShadowRoot): Promise<void> { | ||||
|         // Early activate theme based on media query to prevent light flash | ||||
|         // when dark is preferred | ||||
|         // const pref = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches ? UiThemeEnum.Light : UiThemeEnum.Dark; | ||||
|         // this._activateTheme(root, pref); | ||||
|         this._activateTheme(root, UiThemeEnum.Light); | ||||
|         this._applyTheme(root, globalAK().brand.uiTheme); | ||||
|         this._applyTheme(root, await this.getTheme()); | ||||
|     } | ||||
|  | ||||
| @ -125,6 +128,7 @@ export class AKElement extends LitElement { | ||||
|                             : UiThemeEnum.Dark; | ||||
|                     this._activateTheme(root, theme); | ||||
|                 }; | ||||
|                 this._mediaMatcherHandler(undefined); | ||||
|                 this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler); | ||||
|             } | ||||
|             return; | ||||
| @ -139,7 +143,7 @@ export class AKElement extends LitElement { | ||||
|  | ||||
|     static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined { | ||||
|         if (theme === UiThemeEnum.Dark) { | ||||
|             return ThemeDark; | ||||
|             return _darkTheme; | ||||
|         } | ||||
|         return undefined; | ||||
|     } | ||||
|  | ||||
| @ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css"; | ||||
| import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api"; | ||||
| import { UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| import { AKElement } from "../Base"; | ||||
| import { AKElement, rootInterface } from "../Base"; | ||||
| import { BrandContextController } from "./BrandContextController"; | ||||
| import { ConfigContextController } from "./ConfigContextController"; | ||||
| import { EnterpriseContextController } from "./EnterpriseContextController"; | ||||
| @ -51,8 +51,14 @@ export class Interface extends AKElement implements AkInterface { | ||||
|     } | ||||
|  | ||||
|     _activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void { | ||||
|         super._activateTheme(root, theme); | ||||
|         if (theme === this._activeTheme) { | ||||
|             return; | ||||
|         } | ||||
|         console.debug( | ||||
|             `authentik/interface[${rootInterface()?.tagName.toLowerCase()}]: Enabling theme ${theme}`, | ||||
|         ); | ||||
|         super._activateTheme(document as unknown as DocumentOrShadowRoot, theme); | ||||
|         super._activateTheme(root, theme); | ||||
|     } | ||||
|  | ||||
|     async getTheme(): Promise<UiThemeEnum> { | ||||
|  | ||||
| @ -1,7 +1,8 @@ | ||||
| import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { customEvent } from "@goauthentik/elements/utils/customEvents"; | ||||
|  | ||||
| import { LitElement, html } from "lit"; | ||||
| import { html } from "lit"; | ||||
| import { customElement, property } from "lit/decorators.js"; | ||||
|  | ||||
| import { WithBrandConfig } from "../Interface/brandProvider"; | ||||
| @ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale"; | ||||
| import type { LocaleGetter, LocaleSetter } from "./configureLocale"; | ||||
| import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers"; | ||||
|  | ||||
| const LocaleContextBase = WithBrandConfig(LitElement); | ||||
|  | ||||
| /** | ||||
|  * A component to manage your locale settings. | ||||
|  * | ||||
| @ -25,7 +24,7 @@ const LocaleContextBase = WithBrandConfig(LitElement); | ||||
|  * @fires ak-locale-change - When a valid locale has been swapped in | ||||
|  */ | ||||
| @customElement("ak-locale-context") | ||||
| export class LocaleContext extends LocaleContextBase { | ||||
| export class LocaleContext extends WithBrandConfig(AKElement) { | ||||
|     /// @attribute The text representation of the current locale */ | ||||
|     @property({ attribute: true, type: String }) | ||||
|     locale = DEFAULT_LOCALE; | ||||
| @ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase { | ||||
|             return; | ||||
|         } | ||||
|         locale.locale().then(() => { | ||||
|             console.debug(`Setting Locale to ... ${locale.label()} (${locale.code})`); | ||||
|             console.debug(`authentik/locale: Setting Locale to ${locale.label()} (${locale.code})`); | ||||
|             this.setLocale(locale.code).then(() => { | ||||
|                 window.setTimeout(this.notifyApplication, 0); | ||||
|             }); | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants"; | ||||
| import { AKElement } from "@goauthentik/elements/Base"; | ||||
| import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider"; | ||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| import { customElement } from "lit/decorators.js"; | ||||
| @ -84,7 +85,7 @@ export class SidebarBrand extends WithBrandConfig(AKElement) { | ||||
|             <a href="#/" class="pf-c-page__header-brand-link"> | ||||
|                 <div class="pf-c-brand ak-brand"> | ||||
|                     <img | ||||
|                         src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo} | ||||
|                         src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)} | ||||
|                         alt="authentik Logo" | ||||
|                         loading="lazy" | ||||
|                     /> | ||||
|  | ||||
							
								
								
									
										13
									
								
								web/src/elements/utils/images.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								web/src/elements/utils/images.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import { QUERY_MEDIA_COLOR_LIGHT, rootInterface } from "@goauthentik/elements/Base"; | ||||
|  | ||||
| import { UiThemeEnum } from "@goauthentik/api"; | ||||
|  | ||||
| export function themeImage(rawPath: string) { | ||||
|     let enabledTheme = rootInterface()?.activeTheme; | ||||
|     if (!enabledTheme || enabledTheme === UiThemeEnum.Automatic) { | ||||
|         enabledTheme = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches | ||||
|             ? UiThemeEnum.Light | ||||
|             : UiThemeEnum.Dark; | ||||
|     } | ||||
|     return rawPath.replace("%(theme)s", enabledTheme); | ||||
| } | ||||
| @ -11,6 +11,7 @@ import { WebsocketClient } from "@goauthentik/common/ws"; | ||||
| import { Interface } from "@goauthentik/elements/Interface"; | ||||
| import "@goauthentik/elements/LoadingOverlay"; | ||||
| import "@goauthentik/elements/ak-locale-context"; | ||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | ||||
| import "@goauthentik/flow/sources/apple/AppleLoginInit"; | ||||
| import "@goauthentik/flow/sources/plex/PlexLoginInit"; | ||||
| import "@goauthentik/flow/stages/FlowErrorStage"; | ||||
| @ -430,7 +431,9 @@ export class FlowExecutor extends Interface implements StageHost { | ||||
|     renderChallengeWrapper(): TemplateResult { | ||||
|         const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand"> | ||||
|             <img | ||||
|                 src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}" | ||||
|                 src="${themeImage( | ||||
|                     first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""), | ||||
|                 )}" | ||||
|                 alt="authentik Logo" | ||||
|             /> | ||||
|         </div>`; | ||||
|  | ||||
| @ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils"; | ||||
| import { Interface } from "@goauthentik/elements/Interface"; | ||||
| import "@goauthentik/elements/ak-locale-context"; | ||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | ||||
| import "rapidoc"; | ||||
|  | ||||
| import { CSSResult, TemplateResult, css, html } from "lit"; | ||||
| @ -103,7 +104,9 @@ export class APIBrowser extends Interface { | ||||
|                         <img | ||||
|                             alt="authentik Logo" | ||||
|                             class="logo" | ||||
|                             src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}" | ||||
|                             src="${themeImage( | ||||
|                                 first(this.brand?.brandingLogo, DefaultBrand.brandingLogo), | ||||
|                             )}" | ||||
|                         /> | ||||
|                     </div> | ||||
|                 </rapi-doc> | ||||
|  | ||||
| @ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet"; | ||||
| import "@goauthentik/elements/sidebar/Sidebar"; | ||||
| import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand"; | ||||
| import "@goauthentik/elements/sidebar/SidebarItem"; | ||||
| import { themeImage } from "@goauthentik/elements/utils/images"; | ||||
| import { ROUTES } from "@goauthentik/user/Routes"; | ||||
| import "@patternfly/elements/pf-tooltip/pf-tooltip.js"; | ||||
| import { match } from "ts-pattern"; | ||||
| @ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement { | ||||
|                         <a href="#/" class="pf-c-page__header-brand-link"> | ||||
|                             <img | ||||
|                                 class="pf-c-brand" | ||||
|                                 src="${this.brand.brandingLogo}" | ||||
|                                 src="${themeImage(this.brand.brandingLogo)}" | ||||
|                                 alt="${this.brand.brandingTitle}" | ||||
|                             /> | ||||
|                         </a> | ||||
|  | ||||
| @ -21,6 +21,10 @@ This means that if you want to select a default flow based on policy, you can le | ||||
|  | ||||
| The brand configuration controls the branding title (shown in website document title and several other places), the sidebar/header logo that appears in the upper left of the product interface, and the favicon on a browser tab. | ||||
|  | ||||
| :::info | ||||
| Starting with authentik 2024.6.2, the placeholder `%(theme)s` can be used in the logo configuration option, which will be replaced with the active theme. | ||||
| ::: | ||||
|  | ||||
| ## External user settings | ||||
|  | ||||
| The **Default application** configuration can be used to redirect external users to an application when they successfully authenticate without being sent from a specific application. | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens L.
					Jens L.