tests/e2e: add test for authentication flow in compatibility mode (#14392)

* tests/e2e: add test for authentication flow in compatibility mode

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Add prefix class to CSS for easier debugging of constructed stylesheets.

- Use CSS variables for highlighter.

* web: Fix issue where MDX components apply styles out of order.

* web: Fix hover color.

* web: Fix CSS module types. Clean up globals.

* web: Fix issues surrounding availability of shadow root in compatibility mode.

* web: Fix typo.

* web: Partial fixes for storybook dark theme.

* web: Fix overflow.

* web: Fix issues surrounding competing interfaces attempting to apply styles.

* fix padding in ak-alert in. markdown

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* web: Minimize use of sub-module exports.

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Teffen Ellis <teffen@sister.software>
This commit is contained in:
Jens L.
2025-05-15 16:51:11 +02:00
committed by GitHub
parent 814e438422
commit 0cf6bff93c
39 changed files with 1731 additions and 2435 deletions

View File

@ -1,11 +0,0 @@
import { create } from "@storybook/theming/create";
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
export default create({
base: isDarkMode ? "dark" : "light",
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
});

69
web/.storybook/main.js Normal file
View File

@ -0,0 +1,69 @@
/**
* @file Storybook configuration.
* @import { StorybookConfig } from "@storybook/web-components-vite";
* @import { InlineConfig, Plugin } from "vite";
*/
import { cwd } from "process";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
const NODE_ENV = process.env.NODE_ENV || "development";
const CSSImportPattern = /import [\w\$]+ from .+\.(css)/g;
const JavaScriptFilePattern = /\.m?(js|ts|tsx)$/;
/**
* @satisfies {Plugin<never>}
*/
const inlineCSSPlugin = {
name: "inline-css-plugin",
transform: (source, id) => {
if (!JavaScriptFilePattern.test(id)) return;
const code = source.replace(CSSImportPattern, (match) => {
return `${match}?inline`;
});
return {
code,
};
},
};
/**
* @satisfies {StorybookConfig}
*/
const config = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
viteFinal({ plugins = [], ...config }) {
/**
* @satisfies {InlineConfig}
*/
const mergedConfig = {
...config,
define: {
"process.env.NODE_ENV": JSON.stringify(NODE_ENV),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(process.env.AK_API_BASE_PATH || ""),
},
plugins: [inlineCSSPlugin, ...plugins, postcssLit(), tsconfigPaths()],
};
return mergedConfig;
},
};
export default config;

View File

@ -1,81 +0,0 @@
import replace from "@rollup/plugin-replace";
import type { StorybookConfig } from "@storybook/web-components-vite";
import { cwd } from "process";
import modify from "rollup-plugin-modify";
import postcssLit from "rollup-plugin-postcss-lit";
import tsconfigPaths from "vite-tsconfig-paths";
export const isProdBuild = process.env.NODE_ENV === "production";
export const apiBasePath = process.env.AK_API_BASE_PATH || "";
const importInlinePatterns = [
'import AKGlobal from "(\\.\\./)*common/styles/authentik\\.css',
'import AKGlobal from "@goauthentik/common/styles/authentik\\.css',
'import PF.+ from "@patternfly/patternfly/\\S+\\.css',
'import ThemeDark from "@goauthentik/common/styles/theme-dark\\.css',
'import OneDark from "@goauthentik/common/styles/one-dark\\.css',
'import styles from "\\./LibraryPageImpl\\.css',
];
const importInlineRegexp = new RegExp(importInlinePatterns.map((a) => `(${a})`).join("|"));
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
addons: [
"@storybook/addon-controls",
"@storybook/addon-links",
"@storybook/addon-essentials",
"storybook-addon-mock",
],
staticDirs: [
{
from: "../node_modules/@patternfly/patternfly/patternfly-base.css",
to: "@patternfly/patternfly/patternfly-base.css",
},
{
from: "../src/common/styles/authentik.css",
to: "@goauthentik/common/styles/authentik.css",
},
{
from: "../src/common/styles/theme-dark.css",
to: "@goauthentik/common/styles/theme-dark.css",
},
{
from: "../src/common/styles/one-dark.css",
to: "@goauthentik/common/styles/one-dark.css",
},
],
framework: {
name: "@storybook/web-components-vite",
options: {},
},
docs: {
autodocs: "tag",
},
async viteFinal(config) {
return {
...config,
plugins: [
modify({
find: importInlineRegexp,
replace: (match: RegExpMatchArray) => {
return `${match}?inline`;
},
}),
replace({
"process.env.NODE_ENV": JSON.stringify(
isProdBuild ? "production" : "development",
),
"process.env.CWD": JSON.stringify(cwd()),
"process.env.AK_API_BASE_PATH": JSON.stringify(apiBasePath),
"preventAssignment": true,
}),
...config.plugins,
postcssLit(),
tsconfigPaths(),
],
};
},
};
export default config;

38
web/.storybook/manager.js Normal file
View File

@ -0,0 +1,38 @@
/**
* @file Storybook manager configuration.
*
* @import { ThemeVarsPartial } from "storybook/internal/theming";
*/
import { createUIThemeEffect, resolveUITheme } from "@goauthentik/web/common/theme.ts";
import { addons } from "@storybook/manager-api";
import { create } from "@storybook/theming/create";
/**
* @satisfies {Partial<ThemeVarsPartial>}
*/
const baseTheme = {
brandTitle: "authentik Storybook",
brandUrl: "https://goauthentik.io",
brandImage: "https://goauthentik.io/img/icon_left_brand_colour.svg",
brandTarget: "_self",
};
const uiTheme = resolveUITheme();
addons.setConfig({
theme: create({
...baseTheme,
base: uiTheme,
}),
enableShortcuts: false,
});
createUIThemeEffect((nextUITheme) => {
addons.setConfig({
theme: create({
...baseTheme,
base: nextUITheme,
}),
enableShortcuts: false,
});
});

View File

@ -1,9 +0,0 @@
// .storybook/manager.js
import { addons } from "@storybook/manager-api";
import authentikTheme from "./authentikTheme";
addons.setConfig({
theme: authentikTheme,
enableShortcuts: false,
});

View File

@ -1,5 +1,3 @@
<link rel="stylesheet" href="@patternfly/patternfly/patternfly-base.css" />
<link rel="stylesheet" href="@goauthentik/common/styles/authentik.css" />
<style>
body {
overflow-y: scroll;

32
web/.storybook/preview.js Normal file
View File

@ -0,0 +1,32 @@
/// <reference types="../types/css.js" />
/**
* @file Storybook manager configuration.
*
* @import { Preview } from "@storybook/web-components";
*/
import { applyDocumentTheme } from "@goauthentik/web/common/theme.ts";
applyDocumentTheme();
/**
* @satisfies {Preview}
*/
const preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

View File

@ -1,30 +0,0 @@
import type { Preview } from "@storybook/web-components";
import "@goauthentik/common/styles/authentik.css";
// import "@goauthentik/common/styles/theme-dark.css";
import "@patternfly/patternfly/components/Brand/brand.css";
import "@patternfly/patternfly/components/Page/page.css";
// .storybook/preview.js
import "@patternfly/patternfly/patternfly-base.css";
const preview: Preview = {
parameters: {
options: {
storySort: {
method: "alphabetical",
},
},
actions: { argTypesRegex: "^on[A-Z].*" },
cssUserPrefs: {
"prefers-color-scheme": "light",
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
};
export default preview;

2737
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,14 @@
"exports": {
"./package.json": "./package.json",
"./paths": "./paths.js",
"./scripts/*": "./scripts/*.mjs"
"./scripts/*": "./scripts/*.mjs",
"./elements/*": "./src/elements/*",
"./common/*": "./src/common/*",
"./components/*": "./src/components/*",
"./flow/*": "./src/flow/*",
"./locales/*": "./src/locales/*",
"./user/*": "./src/user/*",
"./admin/*": "./src/admin/*"
},
"dependencies": {
"@codemirror/lang-css": "^6.3.1",
@ -105,14 +112,14 @@
"@hcaptcha/types": "^1.0.4",
"@lit/localize-tools": "^0.8.0",
"@rollup/plugin-replace": "^6.0.1",
"@storybook/addon-essentials": "^8.3.4",
"@storybook/addon-links": "^8.3.4",
"@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.3.4",
"@storybook/builder-vite": "^8.3.4",
"@storybook/manager-api": "^8.3.4",
"@storybook/web-components": "^8.3.4",
"@storybook/web-components-vite": "^8.3.4",
"@storybook/addon-essentials": "^8.6.12",
"@storybook/addon-links": "^8.6.12",
"@storybook/blocks": "^8.6.12",
"@storybook/experimental-addon-test": "^8.6.12",
"@storybook/manager-api": "^8.6.12",
"@storybook/test": "^8.6.12",
"@storybook/web-components": "^8.6.12",
"@storybook/web-components-vite": "^8.6.12",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/chart.js": "^2.9.41",
"@types/codemirror": "^5.60.15",
@ -144,9 +151,8 @@
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",
"pseudolocale": "^2.1.0",
"rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.3.4",
"storybook": "^8.6.12",
"storybook-addon-mock": "^5.0.0",
"turnstile-types": "^1.2.3",
"typescript": "^5.6.2",

View File

@ -86,50 +86,48 @@ export class AdminInterface extends WithLicenseSummary(AuthenticatedInterface) {
//#region Styles
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFButton,
PFDrawer,
PFNav,
css`
.pf-c-page__main,
.pf-c-drawer__content,
.pf-c-page__drawer {
z-index: auto !important;
background-color: transparent;
}
static styles: CSSResult[] = [
PFBase,
PFPage,
PFButton,
PFDrawer,
PFNav,
css`
.pf-c-page__main,
.pf-c-drawer__content,
.pf-c-page__drawer {
z-index: auto !important;
background-color: transparent;
}
.display-none {
display: none;
}
.display-none {
display: none;
}
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
}
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page {
background-color: var(--pf-c-page--BackgroundColor) !important;
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
}
:host([theme="dark"]) {
/* Global page background colour */
.pf-c-page {
--pf-c-page--BackgroundColor: var(--ak-dark-background);
}
}
ak-page-navbar {
grid-area: header;
}
ak-page-navbar {
grid-area: header;
}
.ak-sidebar {
grid-area: nav;
}
.ak-sidebar {
grid-area: nav;
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
}
.pf-c-drawer__panel {
z-index: var(--pf-global--ZIndex--xl);
}
`,
];
//#endregion

View File

@ -21,7 +21,7 @@ import { type LocalTypeCreate } from "./ProviderChoices.js";
@customElement("ak-application-wizard-provider-choice-step")
export class ApplicationWizardProviderChoiceStep extends WithLicenseSummary(ApplicationWizardStep) {
label = msg("Choose A Provider");
label = msg("Choose a Provider");
@state()
failureMessage = "";

View File

@ -1,19 +1,14 @@
import {
CSRFHeaderName,
CSRFMiddleware,
EventMiddleware,
LoggingMiddleware,
} from "@goauthentik/common/api/middleware";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants";
import { globalAK } from "@goauthentik/common/global";
} from "@goauthentik/common/api/middleware.js";
import { EVENT_LOCALE_REQUEST, VERSION } from "@goauthentik/common/constants.js";
import { globalAK } from "@goauthentik/common/global.js";
import { SentryMiddleware } from "@goauthentik/common/sentry";
import { Config, Configuration, CoreApi, CurrentBrand, RootApi } from "@goauthentik/api";
// HACK: Workaround for ESBuild not being able to hoist import statement across entrypoints.
// This can be removed after ESBuild uses a single build context for all entrypoints.
export { CSRFHeaderName };
let globalConfigPromise: Promise<Config> | undefined = Promise.resolve(globalAK().config);
export function config(): Promise<Config> {
if (!globalConfigPromise) {

View File

@ -1,5 +1,5 @@
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants";
import { getCookie } from "@goauthentik/common/utils";
import { EVENT_REQUEST_POST } from "@goauthentik/common/constants.js";
import { getCookie } from "@goauthentik/common/utils.js";
import {
CurrentBrand,

View File

@ -1,3 +1,12 @@
/**
* @file authentik base UI theme.
*/
/* Defined to better identify the base theme when debugging constructed stylesheets. */
.__AK_UI_BASE__ {
--__AK_UI_BASE__: 1;
}
/* #region Global */
:root {

View File

@ -1,42 +1,48 @@
/*
/**
* @file Atom One Dark syntax highlighting theme.
*
* @see https://github.com/atom/one-dark-syntax
*/
Atom One Dark by Daniel Gamage
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
/* Defined to better identify the One Dark theme when debugging constructed stylesheets. */
.__HIGHLIGHT_THEME_ONE_DARK__ {
--__HIGHLIGHT_THEME_ONE_DARK__: 1;
}
base: #282c34
mono-1: #abb2bf
mono-2: #818896
mono-3: #5c6370
hue-1: #56b6c2
hue-2: #61aeee
hue-3: #c678dd
hue-4: #98c379
hue-5: #e06c75
hue-5-2: #be5046
hue-6: #d19a66
hue-6-2: #e6c07b
*/
:root {
--one-dark-base: #282c34;
--one-dark-mono-1: #abb2bf;
--one-dark-mono-2: #818896;
--one-dark-mono-3: #5c6370;
--one-dark-hue-1: #56b6c2;
--one-dark-hue-2: #61aeee;
--one-dark-hue-3: #c678dd;
--one-dark-hue-4: #98c379;
--one-dark-hue-5: #e06c75;
--one-dark-hue-5-2: #be5046;
--one-dark-hue-6: #d19a66;
--one-dark-hue-6-2: #e6c07b;
}
.hljs {
color: #abb2bf;
background: #282c34;
color: var(--one-dark-mono-1);
background: var(--one-dark-base);
}
pre:has(.hljs) {
background: #282c34;
background: var(--one-dark-base);
}
.hljs-comment,
.hljs-quote {
color: #5c6370;
color: var(--one-dark-mono-3);
font-style: italic;
}
.hljs-doctag,
.hljs-keyword,
.hljs-formula {
color: #c678dd;
color: var(--one-dark-hue-3);
}
.hljs-section,
@ -44,11 +50,11 @@ pre:has(.hljs) {
.hljs-selector-tag,
.hljs-deletion,
.hljs-subst {
color: #e06c75;
color: var(--one-dark-hue-5);
}
.hljs-literal {
color: #56b6c2;
color: var(--one-dark-hue-1);
}
.hljs-string,
@ -56,7 +62,7 @@ pre:has(.hljs) {
.hljs-addition,
.hljs-attribute,
.hljs-meta .hljs-string {
color: #98c379;
color: var(--one-dark-hue-4);
}
.hljs-attr,
@ -67,7 +73,7 @@ pre:has(.hljs) {
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-number {
color: #d19a66;
color: var(--one-dark-hue-6);
}
.hljs-symbol,
@ -76,13 +82,13 @@ pre:has(.hljs) {
.hljs-meta,
.hljs-selector-id,
.hljs-title {
color: #61aeee;
color: var(--one-dark-hue-2);
}
.hljs-built_in,
.hljs-title.class_,
.hljs-class .hljs-title {
color: #e6c07b;
color: var(--one-dark-hue-6-2);
}
.hljs-emphasis {

View File

@ -1,3 +1,12 @@
/**
* @file authentik dark UI theme.
*/
/* Defined to better identify the dark theme when debugging constructed stylesheets. */
.__AK_UI_DARK__ {
--__AK_UI_DARK__: 1;
}
/* #region Global */
:root {
@ -5,9 +14,6 @@
--ak-global--Color--100: var(--ak-dark-foreground) !important;
--pf-c-page__main-section--m-light--BackgroundColor: var(--ak-dark-background-darker);
--pf-global--BorderColor--100: var(--ak-dark-background-lighter) !important;
--ak-mermaid-message-text: var(--ak-dark-foreground) !important;
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter) !important;
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
}
body {

View File

@ -1,17 +1,27 @@
/**
* @file Stylesheet utilities.
*/
import { CSSResult, CSSResultOrNative, ReactiveElement, css } from "lit";
import { CSSResultOrNative, ReactiveElement, adoptStyles as adoptStyleSheetsShim, css } from "lit";
/**
* Elements containing adoptable stylesheets.
* Element-like objects containing adoptable stylesheets.
*
* Note that while these all possess the `adoptedStyleSheets` property,
* browser differences and polyfills may make them not actually adoptable.
*
* This type exists to normalize the different ways of accessing the property.
*/
export type StyleSheetParent = Pick<DocumentOrShadowRoot, "adoptedStyleSheets">;
export type StyleRoot =
| Document
| ShadowRoot
| DocumentFragment
| HTMLElement
| DocumentOrShadowRoot;
/**
* Type-predicate to determine if a given object has adoptable stylesheets.
*/
export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheetParent {
export function isStyleRoot(input: StyleRoot): input is ShadowRoot {
// Sanity check - Does the input have the right shape?
if (!input || typeof input !== "object") return false;
@ -25,39 +35,12 @@ export function isAdoptableStyleSheetParent(input: unknown): input is StyleSheet
// All we care about is that it's shaped like an array.
if (!("length" in input.adoptedStyleSheets)) return false;
if (typeof input.adoptedStyleSheets.length !== "number") return false;
// Finally is the array mutable?
return "push" in input.adoptedStyleSheets;
return typeof input.adoptedStyleSheets.length === "number";
}
/**
* Assert that the given input can adopt stylesheets.
*/
export function assertAdoptableStyleSheetParent<T>(
input: T,
): asserts input is T & StyleSheetParent {
if (isAdoptableStyleSheetParent(input)) return;
console.debug("Given input missing `adoptedStyleSheets`", input);
throw new TypeError("Assertion failed: `adoptedStyleSheets` missing in given input");
}
export function resolveStyleSheetParent<T extends HTMLElement | DocumentFragment | Document>(
renderRoot: T,
) {
const styleRoot = "ShadyDOM" in window ? document : renderRoot;
assertAdoptableStyleSheetParent(styleRoot);
return styleRoot;
}
export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
/**
* Given a source of CSS, create a `CSSStyleSheet`.
* Create a lazy-loaded `CSSResult` compatible with Lit's
* element lifecycle.
*
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
*
@ -68,8 +51,12 @@ export type StyleSheetInit = string | CSSResult | CSSStyleSheet;
*
* It works well when Storybook is running in `dev`, but in `build` it fails.
* Storied components will have to map their textual CSS imports.
*
* @see {@linkcode createStyleSheetUnsafe} to create a `CSSStyleSheet` from the given input.
*/
export function createStyleSheet(input: string): CSSResult {
export function createCSSResult(input: string | CSSModule | CSSResultOrNative): CSSResultOrNative {
if (typeof input !== "string") return input;
const inputTemplate = [input] as unknown as TemplateStringsArray;
const result = css(inputTemplate, []);
@ -78,74 +65,91 @@ export function createStyleSheet(input: string): CSSResult {
}
/**
* Given a source of CSS, create a `CSSStyleSheet`.
* Create a `CSSStyleSheet` from the given input, if it is not already a `CSSStyleSheet`.
*
* @see {@linkcode createStyleSheet}
* @throw {@linkcode TypeError} if the input cannot be converted to a `CSSStyleSheet`
*
* @see {@linkcode createCSSResult} for the lazy-loaded `CSSResult` normalization.
*/
export function normalizeCSSSource(css: string): CSSStyleSheet;
export function normalizeCSSSource(styleSheet: CSSStyleSheet): CSSStyleSheet;
export function normalizeCSSSource(cssResult: CSSResult): CSSResult;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative;
export function normalizeCSSSource(input: StyleSheetInit): CSSResultOrNative {
if (typeof input === "string") return createStyleSheet(input);
export function createStyleSheetUnsafe(
input: string | CSSModule | CSSResultOrNative,
): CSSStyleSheet {
const result = typeof input === "string" ? createCSSResult(input) : input;
return input;
}
/**
* Create a `CSSStyleSheet` from the given input.
*/
export function createStyleSheetUnsafe(input: StyleSheetInit): CSSStyleSheet {
const result = normalizeCSSSource(input);
if (result instanceof CSSStyleSheet) return result;
if (!result.styleSheet) {
console.debug(
"authentik/common/stylesheets: CSSResult missing styleSheet, returning empty",
{ result, input },
);
if (result.styleSheet) return result.styleSheet;
throw new TypeError("Expected a CSSStyleSheet");
}
const styleSheet = new CSSStyleSheet();
return result.styleSheet;
styleSheet.replaceSync(result.cssText);
return styleSheet;
}
export type StyleSheetsAction =
| Iterable<CSSStyleSheet>
| ((currentStyleSheets: CSSStyleSheet[]) => Iterable<CSSStyleSheet>);
/**
* Append stylesheet(s) to the given roots.
* Set the adopted stylesheets of a given style parent.
*
* @see {@linkcode removeStyleSheet} to remove a stylesheet from a given roots.
* ```ts
* setAdoptedStyleSheets(document.body, (currentStyleSheets) => [
* ...currentStyleSheets,
* myStyleSheet,
* ]);
* ```
*
* @remarks
* Replacing `adoptedStyleSheets` more than once in the same frame may result in
* the `currentStyleSheets` parameter being out of sync with the actual sheets.
*
* A style root's `adoptedStyleSheets` is a proxy object that only updates when
* DOM is repainted. We can't easily cache the previous entries since the style root
* may polyfilled via ShadyDOM.
*
* Short of using {@linkcode requestAnimationFrame} to sequence the adoption,
* and a visibility toggle to avoid a flash of styles between renders,
* we can't reliably cache the previous entries.
*
* In the meantime, we should try to apply all the sheets in a single frame.
*/
export function appendStyleSheet(
styleParent: StyleSheetParent,
...insertions: CSSStyleSheet[]
): void {
insertions = Array.isArray(insertions) ? insertions : [insertions];
export function setAdoptedStyleSheets(styleRoot: StyleRoot, styleSheets: StyleSheetsAction): void {
let changed = false;
for (const styleSheetInsertion of insertions) {
if (styleParent.adoptedStyleSheets.includes(styleSheetInsertion)) return;
const currentAdoptedStyleSheets = isStyleRoot(styleRoot)
? [...styleRoot.adoptedStyleSheets]
: [];
styleParent.adoptedStyleSheets = [...styleParent.adoptedStyleSheets, styleSheetInsertion];
const result =
typeof styleSheets === "function" ? styleSheets(currentAdoptedStyleSheets) : styleSheets;
const nextAdoptedStyleSheets: CSSStyleSheet[] = [];
for (const [idx, styleSheet] of Array.from(result).entries()) {
const previousStyleSheet = currentAdoptedStyleSheets[idx];
changed ||= previousStyleSheet !== styleSheet;
if (nextAdoptedStyleSheets.includes(styleSheet)) continue;
nextAdoptedStyleSheets.push(styleSheet);
}
changed ||= nextAdoptedStyleSheets.length !== currentAdoptedStyleSheets.length;
if (!changed) return;
if (styleRoot === document) {
document.adoptedStyleSheets = nextAdoptedStyleSheets;
return;
}
adoptStyleSheetsShim(styleRoot as unknown as ShadowRoot, nextAdoptedStyleSheets);
}
/**
* Remove a stylesheet from the given roots, matching by referential equality.
*
* @see {@linkcode appendStyleSheet} to append a stylesheet to a given roots.
*/
export function removeStyleSheet(
styleParent: StyleSheetParent,
...removals: CSSStyleSheet[]
): void {
const nextAdoptedStyleSheets = styleParent.adoptedStyleSheets.filter(
(styleSheet) => !removals.includes(styleSheet),
);
if (nextAdoptedStyleSheets.length === styleParent.adoptedStyleSheets.length) return;
styleParent.adoptedStyleSheets = nextAdoptedStyleSheets;
}
//#region Debugging
/**
* Serialize a stylesheet to a string.
@ -159,8 +163,8 @@ export function serializeStyleSheet(stylesheet: CSSStyleSheet): string {
/**
* Inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheets(styleParent: StyleSheetParent): string[] {
return styleParent.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
export function inspectStyleSheets(styleRoot: ShadowRoot): string[] {
return styleRoot.adoptedStyleSheets.map((styleSheet) => serializeStyleSheet(styleSheet));
}
interface InspectedStyleSheetEntry {
@ -174,8 +178,11 @@ interface InspectedStyleSheetEntry {
* Recursively inspect the adopted stylesheets of a given style parent, serializing them to strings.
*/
export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleSheetEntry {
const styleParent = resolveStyleSheetParent(element.renderRoot);
const styles = inspectStyleSheets(styleParent);
if (!isStyleRoot(element.renderRoot)) {
throw new TypeError("Cannot inspect a render root that doesn't have adoptable stylesheets");
}
const styles = inspectStyleSheets(element.renderRoot);
const tagName = element.tagName.toLowerCase();
const treewalker = document.createTreeWalker(element.renderRoot, NodeFilter.SHOW_ELEMENT, {
@ -186,12 +193,14 @@ export function inspectStyleSheetTree(element: ReactiveElement): InspectedStyleS
return NodeFilter.FILTER_SKIP;
},
});
const children: InspectedStyleSheetEntry[] = [];
let currentNode: Node | null = treewalker.nextNode();
while (currentNode) {
const childElement = currentNode as ReactiveElement;
if (!isAdoptableStyleSheetParent(childElement.renderRoot)) {
if (!isStyleRoot(childElement.renderRoot)) {
currentNode = treewalker.nextNode();
continue;
}
@ -221,3 +230,5 @@ if (process.env.NODE_ENV === "development") {
inspectStyleSheets,
});
}
//#endregion

View File

@ -1,10 +1,47 @@
/**
* @file Theme utilities.
*/
import { UIConfig } from "@goauthentik/common/ui/config";
import {
type StyleRoot,
createStyleSheetUnsafe,
setAdoptedStyleSheets,
} from "@goauthentik/common/stylesheets.js";
import { UIConfig } from "@goauthentik/common/ui/config.js";
import AKBase from "@goauthentik/common/styles/authentik.css";
import AKBaseDark from "@goauthentik/common/styles/theme-dark.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { Config, CurrentBrand, UiThemeEnum } from "@goauthentik/api";
//#region Stylesheet Exports
/**
* A global style sheet for the Patternfly base styles.
*
* @remarks
*
* While a component *may* import its own instance of the PFBase style sheet,
* this instance ensures referential identity.
*/
export const $PFBase = createStyleSheetUnsafe(PFBase);
/**
* A global style sheet for the authentik base styles.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBase = createStyleSheetUnsafe(AKBase);
/**
* A global style sheet for the authentik dark theme.
*
* @see {@linkcode $PFBase} for details.
*/
export const $AKBaseDark = createStyleSheetUnsafe(AKBaseDark);
//#endregion
//#region Scheme Types
/**
@ -134,15 +171,21 @@ export function resolveUITheme(
* Effect listener invoked when the color scheme changes.
*/
export type UIThemeListener = (currentUITheme: ResolvedUITheme) => void;
/**
* Create an effect that runs
* Effect destructor invoked when cleanup is required.
*/
export type UIThemeDestructor = () => void;
/**
* Create an effect that runs UI theme changes.
*
* @returns A cleanup function that removes the effect.
*/
export function createUIThemeEffect(
effect: UIThemeListener,
listenerOptions?: AddEventListenerOptions,
): () => void {
): UIThemeDestructor {
const colorSchemeTarget = resolveUITheme();
const invertedColorSchemeTarget = UIThemeInversion[colorSchemeTarget];
@ -174,6 +217,8 @@ export function createUIThemeEffect(
mediaQueryList.removeEventListener("change", changeListener);
};
listenerOptions?.signal?.addEventListener("abort", cleanup);
return cleanup;
}
@ -181,16 +226,96 @@ export function createUIThemeEffect(
//#region Theme Element
/**
* Applies the current UI theme to the given style root.
*
* @param styleRoot The style root to apply the theme to.
* @param currentUITheme The current UI theme to apply.
* @param additionalStyleSheets Additional style sheets to apply, in addition to the theme's base sheets.
* @category CSS
*
* @see {@linkcode setAdoptedStyleSheets} for caveats.
*/
export function applyUITheme(
styleRoot: StyleRoot,
currentUITheme: ResolvedUITheme = resolveUITheme(),
...additionalStyleSheets: Array<CSSStyleSheet | undefined | null>
): void {
setAdoptedStyleSheets(styleRoot, (currentStyleSheets) => {
const appendedSheets = additionalStyleSheets.filter(Boolean) as CSSStyleSheet[];
if (currentUITheme === UiThemeEnum.Dark) {
return [...currentStyleSheets, $AKBaseDark, ...appendedSheets];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
...appendedSheets,
];
});
}
/**
* Applies the given theme to the document, i.e. the `<html>` element.
*
* @param hint The color scheme hint to use.
*/
export function applyDocumentTheme(hint: CSSColorSchemeValue | UIThemeHint = "auto"): void {
const preferredColorScheme = formatColorScheme(hint);
const applyStyleSheets: UIThemeListener = (currentUITheme) => {
console.debug(`authentik/theme (document): switching to ${currentUITheme} theme`);
setAdoptedStyleSheets(document, (currentStyleSheets) => {
if (currentUITheme === "dark") {
return [...currentStyleSheets, $PFBase, $AKBase, $AKBaseDark];
}
return [
...currentStyleSheets.filter((styleSheet) => styleSheet !== $AKBaseDark),
$PFBase,
$AKBase,
];
});
document.documentElement.dataset.theme = currentUITheme;
};
if (preferredColorScheme === "auto") {
createUIThemeEffect(applyStyleSheets);
return;
}
applyStyleSheets(preferredColorScheme);
}
/**
* An element that can be themed.
*/
export interface ThemedElement extends HTMLElement {
brand?: CurrentBrand;
uiConfig?: UIConfig;
config?: Config;
/**
* The brand information for the current theme.
*/
readonly brand?: CurrentBrand;
/**
* The UI configuration for the current theme,
* typically injected through a Lit Mixin.
*
* @see {@linkcode UIConfig} for details.
*/
readonly uiConfig?: UIConfig;
/**
* An authentik configuration initially provided by the server.
*/
readonly config?: Config;
activeTheme: ResolvedUITheme;
}
/**
* Returns the root interface element of the page.
*
* @todo Can this be handled with a Lit Mixin?
*/
export function rootInterface<T extends ThemedElement = ThemedElement>(): T | null {
const element = document.body.querySelector<T>("[data-ak-interface-root]");

View File

@ -1,5 +1,5 @@
import { me } from "@goauthentik/common/users";
import { isUserRoute } from "@goauthentik/elements/router/utils";
import { me } from "@goauthentik/common/users.js";
import { isUserRoute } from "@goauthentik/elements/router/utils.js";
import { UiThemeEnum, UserSelf } from "@goauthentik/api";
import { CurrentBrand } from "@goauthentik/api";

View File

@ -1,6 +1,6 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants";
import { isResponseErrorLike } from "@goauthentik/common/errors/network";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config.js";
import { EVENT_LOCALE_REQUEST } from "@goauthentik/common/constants.js";
import { isResponseErrorLike } from "@goauthentik/common/errors/network.js";
import { CoreApi, SessionUser } from "@goauthentik/api";

View File

@ -31,18 +31,19 @@ const container = (testItem: TemplateResult) =>
li {
display: block;
}
p {
color: black;
margin-top: 1em;
ak-hint {
--ak-hint--Color: var(--pf-global--Color--dark-100);
}
* {
--ak-hint--Color: black !important;
@media (prefers-color-scheme: dark) {
ak-hint {
--ak-hint--Color: var(--pf-global--Color--light-100);
}
}
ak-hint-title::part(ak-hint-title),
ak-hint-footer::part(ak-hint-footer),
slotted::(*) {
color: black;
p {
margin-top: 1em;
}
</style>

View File

@ -2,7 +2,7 @@ import { AKElement } from "@goauthentik/elements/Base";
import { type SlottedTemplateResult, type Spread } from "@goauthentik/elements/types";
import { spread } from "@open-wc/lit-helpers";
import { html, nothing } from "lit";
import { css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
@ -64,7 +64,15 @@ export class Alert extends AKElement implements IAlert {
icon = "fa-exclamation-circle";
static get styles() {
return [PFBase, PFAlert];
return [
PFBase,
PFAlert,
css`
p {
margin: 0;
}
`,
];
}
get classmap() {

View File

@ -1,30 +1,24 @@
import { globalAK } from "@goauthentik/common/global";
import { globalAK } from "@goauthentik/common/global.js";
import {
StyleSheetInit,
StyleSheetParent,
appendStyleSheet,
StyleRoot,
createCSSResult,
createStyleSheetUnsafe,
removeStyleSheet,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
} from "@goauthentik/common/stylesheets.js";
import {
$AKBase,
CSSColorSchemeValue,
ResolvedUITheme,
UIThemeListener,
ThemedElement,
applyUITheme,
createUIThemeEffect,
formatColorScheme,
resolveUITheme,
} from "@goauthentik/common/theme";
import { type ThemedElement } from "@goauthentik/common/theme";
} from "@goauthentik/common/theme.js";
import { localized } from "@lit/localize";
import { CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { CSSResult, CSSResultGroup, CSSResultOrNative, LitElement } from "lit";
import { property } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import ThemeDark from "@goauthentik/common/styles/theme-dark.css";
import { UiThemeEnum } from "@goauthentik/api";
// Re-export the theme helpers
@ -32,6 +26,58 @@ export { rootInterface } from "@goauthentik/common/theme";
@localized()
export class AKElement extends LitElement implements ThemedElement {
//#region Static Properties
public static styles?: Array<CSSResult | CSSModule>;
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
/**
@ -53,87 +99,54 @@ export class AKElement extends LitElement implements ThemedElement {
//#region Private Properties
readonly #preferredColorScheme: CSSColorSchemeValue;
/**
* The preferred color scheme used to look up the UI theme.
*/
protected readonly preferredColorScheme: CSSColorSchemeValue;
#customCSSStyleSheet: CSSStyleSheet | null;
#darkThemeStyleSheet: CSSStyleSheet | null = null;
/**
* 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;
//#endregion
//#region Lifecycle
protected static finalizeStyles(styles?: CSSResultGroup): CSSResultOrNative[] {
// Ensure all style sheets being passed are really style sheets.
const baseStyles: StyleSheetInit[] = [AKGlobal, OneDark];
if (!styles) return baseStyles.map(createStyleSheetUnsafe);
if (Array.isArray(styles)) {
return [
//---
...(styles as unknown as CSSResultOrNative[]),
...baseStyles,
].flatMap(createStyleSheetUnsafe);
}
return [styles, ...baseStyles].map(createStyleSheetUnsafe);
}
constructor() {
super();
const { brand } = globalAK();
this.#preferredColorScheme = formatColorScheme(brand.uiTheme);
this.activeTheme = resolveUITheme(brand?.uiTheme);
this.#customCSSStyleSheet = brand?.brandingCustomCss
? createStyleSheetUnsafe(brand.brandingCustomCss)
: null;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
protected set styleRoot(nextStyleRoot: StyleRoot | undefined) {
this.#themeAbortController?.abort();
}
#styleRoot?: StyleSheetParent;
this.#styleRoot = nextStyleRoot;
#dispatchTheme: UIThemeListener = (nextUITheme) => {
if (!this.#styleRoot) return;
if (nextUITheme === UiThemeEnum.Dark) {
this.#darkThemeStyleSheet ||= createStyleSheetUnsafe(ThemeDark);
appendStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.activeTheme = UiThemeEnum.Dark;
} else if (this.#darkThemeStyleSheet) {
removeStyleSheet(this.#styleRoot, this.#darkThemeStyleSheet);
this.#darkThemeStyleSheet = null;
this.activeTheme = UiThemeEnum.Light;
}
};
protected createRenderRoot(): HTMLElement | DocumentFragment {
const renderRoot = super.createRenderRoot();
this.#styleRoot = resolveStyleSheetParent(renderRoot);
if (this.#customCSSStyleSheet) {
console.debug(`authentik/element[${this.tagName.toLowerCase()}]: Adding custom CSS`);
appendStyleSheet(this.#styleRoot, this.#customCSSStyleSheet);
}
if (!nextStyleRoot) return;
this.#themeAbortController = new AbortController();
if (this.#preferredColorScheme === "dark") {
this.#dispatchTheme(UiThemeEnum.Dark);
} else if (this.#preferredColorScheme === "auto") {
createUIThemeEffect(this.#dispatchTheme, {
signal: this.#themeAbortController.signal,
});
}
if (this.preferredColorScheme === "dark") {
applyUITheme(nextStyleRoot, UiThemeEnum.Dark, this.#customCSSStyleSheet);
return renderRoot;
this.activeTheme = UiThemeEnum.Dark;
} 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

View File

@ -1,45 +1,45 @@
import {
appendStyleSheet,
createStyleSheetUnsafe,
resolveStyleSheetParent,
} from "@goauthentik/common/stylesheets";
import { ThemedElement } from "@goauthentik/common/theme";
import { UIConfig } from "@goauthentik/common/ui/config";
import { AKElement } from "@goauthentik/elements/Base";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController";
import { globalAK } from "@goauthentik/common/global.js";
import { ThemedElement, applyDocumentTheme } from "@goauthentik/common/theme.js";
import { UIConfig } from "@goauthentik/common/ui/config.js";
import { AKElement } from "@goauthentik/elements/Base.js";
import { VersionContextController } from "@goauthentik/elements/Interface/VersionContextController.js";
import { ModalOrchestrationController } from "@goauthentik/elements/controllers/ModalOrchestrationController.js";
import { state } from "lit/decorators.js";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import type { Config, CurrentBrand, LicenseSummary, Version } from "@goauthentik/api";
import {
type Config,
type CurrentBrand,
type LicenseSummary,
type Version,
} from "@goauthentik/api";
import { BrandContextController } from "./BrandContextController";
import { ConfigContextController } from "./ConfigContextController";
import { EnterpriseContextController } from "./EnterpriseContextController";
import { BrandContextController } from "./BrandContextController.js";
import { ConfigContextController } from "./ConfigContextController.js";
import { EnterpriseContextController } from "./EnterpriseContextController.js";
const configContext = Symbol("configContext");
const modalController = Symbol("modalController");
const versionContext = Symbol("versionContext");
export abstract class LightInterface extends AKElement implements ThemedElement {
protected static readonly PFBaseStyleSheet = createStyleSheetUnsafe(PFBase);
constructor() {
super();
const styleParent = resolveStyleSheetParent(document);
this.dataset.akInterfaceRoot = this.tagName.toLowerCase();
appendStyleSheet(styleParent, Interface.PFBaseStyleSheet);
if (!document.documentElement.dataset.theme) {
applyDocumentTheme(globalAK().brand.uiTheme);
}
}
}
export abstract class Interface extends LightInterface implements ThemedElement {
[configContext]: ConfigContextController;
static styles = [PFBase];
protected [configContext]: ConfigContextController;
[modalController]: ModalOrchestrationController;
protected [modalController]: ModalOrchestrationController;
@state()
public config?: Config;
@ -49,6 +49,7 @@ export abstract class Interface extends LightInterface implements ThemedElement
constructor() {
super();
this.addController(new BrandContextController(this));
this[configContext] = new ConfigContextController(this);
this[modalController] = new ModalOrchestrationController(this);

View File

@ -27,11 +27,14 @@ import remarkGFM from "remark-gfm";
import remarkMdxFrontmatter from "remark-mdx-frontmatter";
import remarkParse from "remark-parse";
import { CSSResult, css } from "lit";
import { css } from "lit";
import { customElement, property } from "lit/decorators.js";
import OneDark from "@goauthentik/common/styles/one-dark.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { UiThemeEnum } from "@goauthentik/api";
@ -67,80 +70,96 @@ export class AKMDX extends AKElement {
resolvedHTML = "";
static get styles(): CSSResult[] {
return [
PFList,
PFContent,
css`
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
static styles = [
PFBase,
PFList,
PFTable,
PFContent,
OneDark,
css`
:host {
--ak-mermaid-message-text: var(--pf-c-content--Color);
--ak-table-stripe-background: var(--pf-global--BackgroundColor--light-200);
}
:host([theme="dark"]) {
--ak-mermaid-message-text: var(--ak-dark-foreground);
--ak-mermaid-box-background-color: var(--ak-dark-background-lighter);
--ak-table-stripe-background: var(--pf-global--BackgroundColor--dark-200);
}
ak-alert + p {
margin-block-start: var(--pf-global--spacer--md);
}
a {
--pf-global--link--Color: var(--pf-global--link--Color--light);
--pf-global--link--Color--hover: var(--pf-global--link--Color--light--hover);
--pf-global--link--Color--visited: var(--pf-global--link--Color);
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
h2:first-of-type {
margin-top: 0;
}
table thead,
table tr:nth-child(2n) {
background-color: var(--ak-table-stripe-background,);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
pre {
overflow-x: auto;
}
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
/*
Note that order of anchor pseudo-selectors must follow:
1. link
2. visited
3. hover
4. active
*/
a:link {
color: var(--pf-global--link--Color);
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
a:visited {
color: var(--pf-global--link--Color--visited);
}
a:hover {
color: var(--pf-global--link--Color--hover);
}
a:active {
color: var(--pf-global--link--Color);
}
h2:first-of-type {
margin-top: 0;
}
table thead,
table tr:nth-child(2n) {
background-color: var(
--ak-table-stripe-background,
var(--pf-global--BackgroundColor--light-200)
);
}
table td,
table th {
border: var(--pf-table-border-width) solid var(--ifm-table-border-color);
padding: var(--pf-global--spacer--md);
}
pre:has(.hljs) {
padding: var(--pf-global--spacer--md);
}
svg[id^="mermaid-svg-"] {
.rect {
fill: var(
--ak-mermaid-box-background-color,
var(--pf-global--BackgroundColor--light-300)
) !important;
}
.messageText {
stroke-width: 4;
fill: var(--ak-mermaid-message-text) !important;
paint-order: stroke;
}
}
`,
];
}
}
`,
];
public async connectedCallback() {
super.connectedCallback();

View File

@ -16,5 +16,5 @@ export const MDXWrapper: React.FC<MDXWrapperProps> = ({ children, frontmatter })
nextChildren.unshift(<h1 key="header-title">{title}</h1>);
}
return <>{nextChildren}</>;
return <div className="pf-c-content">{nextChildren}</div>;
};

View File

@ -40,7 +40,7 @@ export class FormGroup extends AKElement {
* restructured to allow for this.
*/
.pf-c-form__field-group:has(.pf-c-form__field-group-header:hover) .pf-c-button {
color: var(--pf-global--Color--100) !important;
color: var(--pf-c-button--m-plain--hover--Color) !important;
}
/**

View File

@ -1,21 +1,16 @@
import {
appendStyleSheet,
assertAdoptableStyleSheetParent,
createStyleSheetUnsafe,
} from "@goauthentik/common/stylesheets.js";
import { applyDocumentTheme } from "@goauthentik/common/theme.js";
import { TemplateResult, render as litRender } from "lit";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
// A special version of render that ensures our style sheets will always be available
// to all elements under test. Ensures they look right during testing, and that any
// CSS-based checks for visibility will return correct values.
/**
* A special version of render that ensures our stylesheets:
*
* - Will always be available to all elements under test.
* - Ensure they look right during testing.
* - CSS-based checks for visibility will return correct values.
*/
export const render = (body: TemplateResult) => {
assertAdoptableStyleSheetParent(document);
applyDocumentTheme();
appendStyleSheet(document, ...[PFBase, AKGlobal].map(createStyleSheetUnsafe));
return litRender(body, document.body);
};

View File

@ -1,6 +1,11 @@
import { type LitElement, type ReactiveControllerHost, type TemplateResult, nothing } from "lit";
import "lit";
/**
* Type utility to make readonly properties mutable.
*/
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
/**
* A custom element which may be used as a host for a ReactiveController.
*
@ -8,7 +13,7 @@ import "lit";
*
* This type is derived from an internal type in Lit.
*/
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & T> & HTMLElement;
export type ReactiveElementHost<T> = Partial<ReactiveControllerHost & Writeable<T>> & HTMLElement;
export type AbstractLitElementConstructor = abstract new (...args: never[]) => LitElement;

View File

@ -36,7 +36,7 @@ export const PasswordManagerPrefill: {
totp: undefined,
};
export const OR_LIST_FORMATTERS = new Intl.ListFormat("default", {
export const OR_LIST_FORMATTERS: Intl.ListFormat = new Intl.ListFormat("default", {
style: "short",
type: "disjunction",
});

View File

@ -1,13 +1,13 @@
// sort-imports-ignore
import "rapidoc";
import "@goauthentik/elements/ak-locale-context/index.js";
import { CSRFHeaderName } from "@goauthentik/common/api/config";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants";
import { getCookie } from "@goauthentik/common/utils";
import { Interface } from "@goauthentik/elements/Interface";
import "@goauthentik/elements/ak-locale-context";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import { themeImage } from "@goauthentik/elements/utils/images";
import { CSRFHeaderName } from "@goauthentik/common/api/middleware.js";
import { EVENT_THEME_CHANGE } from "@goauthentik/common/constants.js";
import { getCookie } from "@goauthentik/common/utils.js";
import { Interface } from "@goauthentik/elements/Interface/Interface.js";
import { DefaultBrand } from "@goauthentik/common/ui/config.js";
import { themeImage } from "@goauthentik/elements/utils/images.js";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";

View File

@ -1,7 +1,7 @@
import { LightInterface } from "@goauthentik/elements/Interface";
import { msg } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import PFEmptyState from "@patternfly/patternfly/components/EmptyState/empty-state.css";
@ -11,19 +11,17 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
@customElement("ak-loading")
export class Loading extends LightInterface {
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFSpinner,
PFEmptyState,
css`
:host([theme="dark"]) h1 {
color: var(--ak-dark-foreground);
}
`,
];
}
static styles = [
PFBase,
PFPage,
PFSpinner,
PFEmptyState,
css`
:host([theme="dark"]) h1 {
color: var(--ak-dark-foreground);
}
`,
];
render(): TemplateResult {
return html` <section

23
web/types/css.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
/**
* @file ESBuild CSS module type definitions.
*/
declare module "*.css" {
import { CSSResult } from "lit";
global {
/**
* A branded type representing a CSS file imported by ESBuild.
*
* While this is a `string`, this is typed as a {@linkcode CSSResult}
* to satisfy LitElement's `static styles` property.
*/
export type CSSModule = CSSResult & { readonly __brand?: string };
}
/**
* The text content of a CSS file imported by ESBuild.
*/
const css: CSSModule;
export default css;
}

21
web/types/lit.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
/**
* @file Lit-specific globals applied to the Window object.
*/
export {};
declare global {
interface HTMLElement {
/**
* A property defined by Lit to track the element part.
*/
_$litPart$?: unknown;
}
interface Window {
/**
* A possible nonce to use create a CSP-safe style element.
*/
litNonce?: string;
}
}

View File

@ -1,5 +1,3 @@
declare module "*.css";
declare module "*.md" {
/**
* The serialized JSON content of an MD file.
@ -15,10 +13,3 @@ declare module "*.mdx" {
const serializedJSON: string;
export default serializedJSON;
}
declare namespace Intl {
class ListFormat {
constructor(locale: string, args: { [key: string]: string });
public format: (items: string[]) => string;
}
}