import { globalAK } from "#common/global"; import { StyleRoot, createCSSResult, createStyleSheetUnsafe } from "#common/stylesheets"; import { $AKBase, CSSColorSchemeValue, ResolvedUITheme, applyUITheme, createUIThemeEffect, formatColorScheme, resolveUITheme, } from "#common/theme"; import { localized } from "@lit/localize"; import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit"; import { property } from "lit/decorators.js"; import { UiThemeEnum } from "@goauthentik/api"; @localized() export class AKElement extends LitElement { //#region Static Properties public static styles?: Array; protected static override finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] { if (!styles) return [$AKBase]; if (!Array.isArray(styles)) return [createCSSResult(styles), $AKBase]; return [ // --- ...(styles.flat() as CSSResultOrNative[]).map(createCSSResult), $AKBase, ]; } //#endregion //#region Lifecycle constructor() { super(); const { brand } = globalAK(); this.preferredColorScheme = formatColorScheme(brand.uiTheme); this.activeTheme = resolveUITheme(brand?.uiTheme); this.#customCSSStyleSheet = brand?.brandingCustomCss ? createStyleSheetUnsafe(brand.brandingCustomCss) : null; } public override disconnectedCallback(): void { this.#themeAbortController?.abort(); super.disconnectedCallback(); } /** * Returns the node into which the element should render. * * @see {LitElement.createRenderRoot} for more information. */ protected override createRenderRoot(): HTMLElement | DocumentFragment { const renderRoot = super.createRenderRoot(); this.styleRoot ??= renderRoot; return renderRoot; } //#endregion //#region Properties /** * The resolved theme of the current element. * * @remarks * * Unlike the browser's current color scheme, this is a value that can be * resolved to a specific theme, i.e. dark or light. */ @property({ attribute: "theme", type: String, reflect: true, }) public activeTheme: ResolvedUITheme; //#endregion //#region Private Properties /** * The preferred color scheme used to look up the UI theme. */ protected readonly preferredColorScheme: CSSColorSchemeValue; /** * A custom CSS style sheet to apply to the element. */ readonly #customCSSStyleSheet: CSSStyleSheet | null; /** * A controller to abort theme updates, such as when the element is disconnected. */ #themeAbortController: AbortController | null = null; /** * The style root to which the theme is applied. */ #styleRoot?: StyleRoot; protected set styleRoot(nextStyleRoot: StyleRoot | undefined) { this.#themeAbortController?.abort(); this.#styleRoot = nextStyleRoot; if (!nextStyleRoot) return; this.#themeAbortController = new AbortController(); if (this.preferredColorScheme === "dark") { applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet); this.activeTheme = UiThemeEnum.Dark; } else if (this.preferredColorScheme === "light") { applyUITheme(nextStyleRoot, UiThemeEnum.Light, this.#customCSSStyleSheet); this.activeTheme = UiThemeEnum.Light; } else if (this.preferredColorScheme === "auto") { createUIThemeEffect( (nextUITheme) => { applyUITheme(nextStyleRoot, nextUITheme, this.#customCSSStyleSheet); this.activeTheme = nextUITheme; }, { signal: this.#themeAbortController.signal, }, ); } } protected get styleRoot(): StyleRoot | undefined { return this.#styleRoot; } //#endregion }