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();
|
this.centerText = outposts.pagination.count.toString();
|
||||||
|
outpostStats.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
return outpostStats;
|
return outpostStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
|
||||||
|
import { globalAK } from "@goauthentik/common/global";
|
||||||
import { UIConfig } from "@goauthentik/common/ui/config";
|
import { UIConfig } from "@goauthentik/common/ui/config";
|
||||||
import { adaptCSS } from "@goauthentik/common/utils";
|
import { adaptCSS } from "@goauthentik/common/utils";
|
||||||
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
import { ensureCSSStyleSheet } from "@goauthentik/elements/utils/ensureCSSStyleSheet";
|
||||||
@ -16,6 +17,7 @@ type AkInterface = HTMLElement & {
|
|||||||
brand?: CurrentBrand;
|
brand?: CurrentBrand;
|
||||||
uiConfig?: UIConfig;
|
uiConfig?: UIConfig;
|
||||||
config?: Config;
|
config?: Config;
|
||||||
|
get activeTheme(): UiThemeEnum | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
export const rootInterface = <T extends AkInterface>(): T | undefined =>
|
||||||
@ -41,7 +43,11 @@ function fetchCustomCSS(): Promise<string[]> {
|
|||||||
return css;
|
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()
|
@localized()
|
||||||
export class AKElement extends LitElement {
|
export class AKElement extends LitElement {
|
||||||
@ -77,8 +83,7 @@ export class AKElement extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(): Promise<UiThemeEnum> {
|
async getTheme(): Promise<UiThemeEnum> {
|
||||||
// return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
|
return rootInterface()?.getTheme() || UiThemeEnum.Automatic;
|
||||||
return rootInterface()?.getTheme() || UiThemeEnum.Light;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fixElementStyles() {
|
fixElementStyles() {
|
||||||
@ -91,9 +96,7 @@ export class AKElement extends LitElement {
|
|||||||
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
async _initTheme(root: DocumentOrShadowRoot): Promise<void> {
|
||||||
// Early activate theme based on media query to prevent light flash
|
// Early activate theme based on media query to prevent light flash
|
||||||
// when dark is preferred
|
// when dark is preferred
|
||||||
// const pref = window.matchMedia(QUERY_MEDIA_COLOR_LIGHT).matches ? UiThemeEnum.Light : UiThemeEnum.Dark;
|
this._applyTheme(root, globalAK().brand.uiTheme);
|
||||||
// this._activateTheme(root, pref);
|
|
||||||
this._activateTheme(root, UiThemeEnum.Light);
|
|
||||||
this._applyTheme(root, await this.getTheme());
|
this._applyTheme(root, await this.getTheme());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,6 +128,7 @@ export class AKElement extends LitElement {
|
|||||||
: UiThemeEnum.Dark;
|
: UiThemeEnum.Dark;
|
||||||
this._activateTheme(root, theme);
|
this._activateTheme(root, theme);
|
||||||
};
|
};
|
||||||
|
this._mediaMatcherHandler(undefined);
|
||||||
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
this._mediaMatcher.addEventListener("change", this._mediaMatcherHandler);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@ -139,7 +143,7 @@ export class AKElement extends LitElement {
|
|||||||
|
|
||||||
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
static themeToStylesheet(theme?: UiThemeEnum): CSSStyleSheet | undefined {
|
||||||
if (theme === UiThemeEnum.Dark) {
|
if (theme === UiThemeEnum.Dark) {
|
||||||
return ThemeDark;
|
return _darkTheme;
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
|||||||
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
import type { Config, CurrentBrand, LicenseSummary } from "@goauthentik/api";
|
||||||
import { UiThemeEnum } from "@goauthentik/api";
|
import { UiThemeEnum } from "@goauthentik/api";
|
||||||
|
|
||||||
import { AKElement } from "../Base";
|
import { AKElement, rootInterface } from "../Base";
|
||||||
import { BrandContextController } from "./BrandContextController";
|
import { BrandContextController } from "./BrandContextController";
|
||||||
import { ConfigContextController } from "./ConfigContextController";
|
import { ConfigContextController } from "./ConfigContextController";
|
||||||
import { EnterpriseContextController } from "./EnterpriseContextController";
|
import { EnterpriseContextController } from "./EnterpriseContextController";
|
||||||
@ -51,8 +51,14 @@ export class Interface extends AKElement implements AkInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_activateTheme(root: DocumentOrShadowRoot, theme: UiThemeEnum): void {
|
_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(document as unknown as DocumentOrShadowRoot, theme);
|
||||||
|
super._activateTheme(root, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getTheme(): Promise<UiThemeEnum> {
|
async getTheme(): Promise<UiThemeEnum> {
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
|
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 { customEvent } from "@goauthentik/elements/utils/customEvents";
|
||||||
|
|
||||||
import { LitElement, html } from "lit";
|
import { html } from "lit";
|
||||||
import { customElement, property } from "lit/decorators.js";
|
import { customElement, property } from "lit/decorators.js";
|
||||||
|
|
||||||
import { WithBrandConfig } from "../Interface/brandProvider";
|
import { WithBrandConfig } from "../Interface/brandProvider";
|
||||||
@ -9,8 +10,6 @@ import { initializeLocalization } from "./configureLocale";
|
|||||||
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
import type { LocaleGetter, LocaleSetter } from "./configureLocale";
|
||||||
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
import { DEFAULT_LOCALE, autoDetectLanguage, getBestMatchLocale } from "./helpers";
|
||||||
|
|
||||||
const LocaleContextBase = WithBrandConfig(LitElement);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to manage your locale settings.
|
* 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
|
* @fires ak-locale-change - When a valid locale has been swapped in
|
||||||
*/
|
*/
|
||||||
@customElement("ak-locale-context")
|
@customElement("ak-locale-context")
|
||||||
export class LocaleContext extends LocaleContextBase {
|
export class LocaleContext extends WithBrandConfig(AKElement) {
|
||||||
/// @attribute The text representation of the current locale */
|
/// @attribute The text representation of the current locale */
|
||||||
@property({ attribute: true, type: String })
|
@property({ attribute: true, type: String })
|
||||||
locale = DEFAULT_LOCALE;
|
locale = DEFAULT_LOCALE;
|
||||||
@ -78,7 +77,7 @@ export class LocaleContext extends LocaleContextBase {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
locale.locale().then(() => {
|
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(() => {
|
this.setLocale(locale.code).then(() => {
|
||||||
window.setTimeout(this.notifyApplication, 0);
|
window.setTimeout(this.notifyApplication, 0);
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
import { EVENT_SIDEBAR_TOGGLE } from "@goauthentik/common/constants";
|
||||||
import { AKElement } from "@goauthentik/elements/Base";
|
import { AKElement } from "@goauthentik/elements/Base";
|
||||||
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
import { customElement } from "lit/decorators.js";
|
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">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<div class="pf-c-brand ak-brand">
|
<div class="pf-c-brand ak-brand">
|
||||||
<img
|
<img
|
||||||
src=${this.brand?.brandingLogo ?? DefaultBrand.brandingLogo}
|
src=${themeImage(this.brand?.brandingLogo ?? DefaultBrand.brandingLogo)}
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
loading="lazy"
|
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 { Interface } from "@goauthentik/elements/Interface";
|
||||||
import "@goauthentik/elements/LoadingOverlay";
|
import "@goauthentik/elements/LoadingOverlay";
|
||||||
import "@goauthentik/elements/ak-locale-context";
|
import "@goauthentik/elements/ak-locale-context";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import "@goauthentik/flow/sources/apple/AppleLoginInit";
|
import "@goauthentik/flow/sources/apple/AppleLoginInit";
|
||||||
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
import "@goauthentik/flow/sources/plex/PlexLoginInit";
|
||||||
import "@goauthentik/flow/stages/FlowErrorStage";
|
import "@goauthentik/flow/stages/FlowErrorStage";
|
||||||
@ -430,7 +431,9 @@ export class FlowExecutor extends Interface implements StageHost {
|
|||||||
renderChallengeWrapper(): TemplateResult {
|
renderChallengeWrapper(): TemplateResult {
|
||||||
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
const logo = html`<div class="pf-c-login__main-header pf-c-brand ak-brand">
|
||||||
<img
|
<img
|
||||||
src="${first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, "")}"
|
src="${themeImage(
|
||||||
|
first(this.brand?.brandingLogo, globalAK()?.brand.brandingLogo, ""),
|
||||||
|
)}"
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
/>
|
/>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
@ -5,6 +5,7 @@ import { first, getCookie } from "@goauthentik/common/utils";
|
|||||||
import { Interface } from "@goauthentik/elements/Interface";
|
import { Interface } from "@goauthentik/elements/Interface";
|
||||||
import "@goauthentik/elements/ak-locale-context";
|
import "@goauthentik/elements/ak-locale-context";
|
||||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import "rapidoc";
|
import "rapidoc";
|
||||||
|
|
||||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||||
@ -103,7 +104,9 @@ export class APIBrowser extends Interface {
|
|||||||
<img
|
<img
|
||||||
alt="authentik Logo"
|
alt="authentik Logo"
|
||||||
class="logo"
|
class="logo"
|
||||||
src="${first(this.brand?.brandingLogo, DefaultBrand.brandingLogo)}"
|
src="${themeImage(
|
||||||
|
first(this.brand?.brandingLogo, DefaultBrand.brandingLogo),
|
||||||
|
)}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</rapi-doc>
|
</rapi-doc>
|
||||||
|
@ -21,6 +21,7 @@ import "@goauthentik/elements/router/RouterOutlet";
|
|||||||
import "@goauthentik/elements/sidebar/Sidebar";
|
import "@goauthentik/elements/sidebar/Sidebar";
|
||||||
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
import { DefaultBrand } from "@goauthentik/elements/sidebar/SidebarBrand";
|
||||||
import "@goauthentik/elements/sidebar/SidebarItem";
|
import "@goauthentik/elements/sidebar/SidebarItem";
|
||||||
|
import { themeImage } from "@goauthentik/elements/utils/images";
|
||||||
import { ROUTES } from "@goauthentik/user/Routes";
|
import { ROUTES } from "@goauthentik/user/Routes";
|
||||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
@ -193,7 +194,7 @@ class UserInterfacePresentation extends AKElement {
|
|||||||
<a href="#/" class="pf-c-page__header-brand-link">
|
<a href="#/" class="pf-c-page__header-brand-link">
|
||||||
<img
|
<img
|
||||||
class="pf-c-brand"
|
class="pf-c-brand"
|
||||||
src="${this.brand.brandingLogo}"
|
src="${themeImage(this.brand.brandingLogo)}"
|
||||||
alt="${this.brand.brandingTitle}"
|
alt="${this.brand.brandingTitle}"
|
||||||
/>
|
/>
|
||||||
</a>
|
</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.
|
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
|
## 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.
|
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