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