Compare commits

..

5 Commits

Author SHA1 Message Date
Teffen Ellis
da9ec72b25 Fix wizards to use consistent height. 2026-05-08 01:55:21 +02:00
Jens Langhammer
d83250fb25 adjust labels for service account
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-05-08 01:55:20 +02:00
Jens Langhammer
449fe80887 elements/wizard: deactivate side nav buttons if wizard is done
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2026-05-08 01:55:20 +02:00
Dewi Roberts
b4c7dea4e8 website/integrations: ? (#22138)
Add ? to all integrations opening headers
2026-05-07 22:32:27 +00:00
Dominic R
100ae2b355 website/docs: add CMake to full dev environment (#22137) 2026-05-07 18:08:59 -04:00
210 changed files with 655 additions and 3450 deletions

View File

@@ -48,7 +48,7 @@
<ak-skip-to-content></ak-skip-to-content>
<ak-message-container></ak-message-container>
<ak-drawer id="flow-drawer">
<ak-flow
<ak-flow-executor
slug="{{ flow.slug }}"
class="pf-c-login"
data-layout="{{ flow.layout|default:'stacked' }}"
@@ -57,7 +57,7 @@
{% include "base/placeholder.html" %}
<ak-brand-links name="flow-links" slot="footer"></ak-brand-links>
</ak-flow>
</ak-flow-executor>
<ak-flow-inspector
slot="panel"

1043
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,11 +12,8 @@
"extract-locales": "lit-localize extract",
"format": "wireit",
"lint": "eslint --fix .",
"lint:css": "stylelint ./src/**/*.css",
"lint:css-in-js": "stylelint --custom-syntax postcss-lit ./src/**/*.styles.ts",
"lint:imports": "knip --config scripts/knip.config.ts",
"lint:lockfile": "wireit",
"lint:styles": "run-p lint:css lint:css-in-js",
"lint:types": "wireit",
"lint-check": "eslint --max-warnings 0 .",
"lit-analyse": "wireit",
@@ -170,7 +167,6 @@
"pino": "^10.3.1",
"pino-pretty": "^13.1.2",
"playwright": "^1.58.2",
"postcss-lit": "^1.4.1",
"prettier": "^3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
"pseudolocale": "^2.2.0",
@@ -187,8 +183,6 @@
"remark-mdx-frontmatter": "^5.2.0",
"storybook": "^10.2.1",
"style-mod": "^4.1.3",
"stylelint": "^17.6.0",
"stylelint-config-standard": "^40.0.0",
"trusted-types": "^2.0.0",
"ts-pattern": "^5.9.0",
"turnstile-types": "^1.2.3",

View File

@@ -11,6 +11,8 @@ import { CreateWizard } from "#elements/wizard/CreateWizard";
import { TypeCreateWizardPageLayouts } from "#elements/wizard/TypeCreateWizardPage";
import { WizardPage } from "#elements/wizard/WizardPage";
import { ButtonKindLabelRecord } from "#components/ak-wizard/shared";
import { UserForm } from "#admin/users/UserForm";
import { TypeCreate, UserServiceAccountResponse, UserTypeEnum } from "@goauthentik/api";
@@ -57,7 +59,7 @@ export interface UserWizardState {
export class ServiceAccountResultPage extends WizardPage<UserWizardState> {
public static styles: CSSResult[] = [PFForm, PFFormControl];
public override headline = msg("Review Credentials");
public override headline = msg("View Credentials");
@state()
protected result: UserServiceAccountResponse | null = null;
@@ -75,6 +77,10 @@ export class ServiceAccountResultPage extends WizardPage<UserWizardState> {
this.host.cancelable = false;
};
public formatNextLabel(): SlottedTemplateResult | null {
return ButtonKindLabelRecord.close;
}
public override nextCallback = async (): Promise<boolean> => true;
protected override render(): SlottedTemplateResult {

View File

@@ -58,9 +58,10 @@ export abstract class WizardStep extends AKElement {
.pf-c-wizard__main-body {
display: flex;
flex-flow: row wrap;
flex-flow: column;
& > * {
width: 100%;
flex: 1 1 auto;
}
}

View File

@@ -1,73 +1,32 @@
import AKDrawer from "./ak-drawer.styles";
import { DrawerResizeController } from "./drawerResizeController";
import Style from "./ak-drawer.css";
import { html, LitElement, nothing, PropertyValues } from "lit";
import { AKElement } from "#elements/Base";
import { classList } from "#elements/directives/class-list";
import { html } from "lit";
import { property } from "lit/decorators.js";
export class DrawerExpandRequest extends Event {
static readonly eventName = "ak-drawer-expand-request";
expanded: boolean | null = null;
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
constructor(expanded: boolean | null = null) {
super(DrawerExpandRequest.eventName, { bubbles: true, composed: true });
this.expanded = expanded;
}
}
export class AkDrawer extends LitElement {
static readonly styles = [AKDrawer];
@property({ type: Boolean })
public resizable = false;
export class Drawer extends AKElement {
static readonly styles = [PFDrawer, Style];
@property({ type: Boolean, reflect: true })
public expanded = false;
public open = false;
@property({ type: Boolean, reflect: true })
public resizing = false;
render() {
const open = [(this.open && "pf-m-expanded") || "pf-m-collapsed"];
@property({ type: String, reflect: true })
public width = "33";
private resize = new DrawerResizeController(this);
onDrawerRequest = (ev: DrawerExpandRequest) => {
ev.stopPropagation();
this.expanded = ev.expanded === null ? !this.expanded : ev.expanded;
};
constructor() {
super();
this.addEventListener(DrawerExpandRequest.eventName, this.onDrawerRequest);
}
public override render() {
return html`
<div class="ak-v2-c-drawer" part="drawer">
<div class="ak-v2-c-drawer__main" part="drawer-main">
<div class="ak-v2-c-drawer__content" part="drawer-content">
<div class="ak-v2-c-drawer__body" part="drawer-body">
<slot></slot>
<div class="pf-c-page__drawer">
<div class="pf-c-drawer ${classList(open)}" id="flow-drawer">
<div class="pf-c-drawer__main">
<div class="pf-c-drawer__content">
<div class="pf-c-drawer__body">
<slot></slot>
</div>
</div>
</div>
<div class="ak-v2-c-drawer__panel" part="drawer-panel">
${this.resizable
? html` <div
class="ak-v2-c-drawer__splitter"
part="drawer-splitter"
@mousedown=${this.resize.handleMouseDown}
@keydown=${this.resize.handleKeyDown}
@touchstart=${this.resize.handleTouchStart}
role="separator"
tabindex="0"
>
<div
class="ak-v2-c-drawer__splitter-handle"
aria-hidden="true"
></div>
</div>`
: nothing}
<div class="ak-v2-c-drawer__panel-main" part="drawer-panel-main">
<div class="pf-c-drawer__panel pf-m-width-33">
<slot name="panel"></slot>
</div>
</div>
@@ -75,26 +34,4 @@ export class AkDrawer extends LitElement {
</div>
`;
}
public override updated(changed: PropertyValues<this>) {
super.updated(changed);
// Simulate the behavior of summary/details, another disclosure pattern.
const expanded = changed.get("expanded");
if (expanded !== undefined) {
const expandedMsg = (i: boolean) => (i ? "open" : "closed");
this.dispatchEvent(
new ToggleEvent("toggle", {
newState: expandedMsg(this.expanded),
oldState: expandedMsg(expanded),
}),
);
}
}
}
declare global {
interface GlobalEventHandlersEventMap {
[DrawerExpandRequest.eventName]: DrawerExpandRequest;
}
}

View File

@@ -0,0 +1,40 @@
slot {
display: content;
}
[data-theme="dark"] {
--pf-c-drawer__panel--BackgroundColor: var(--ak-dark-background);
}
.pf-c-drawer {
/* TODO: Revisit this after native <dialog> modals are implemented. */
--pf-c-drawer__content--ZIndex: auto;
}
.pf-c-drawer__body {
display: flex;
flex-flow: column;
}
.pf-c-drawer__content {
--pf-c-drawer__content--BackgroundColor: transparent;
}
.pf-c-drawer {
.pf-c-drawer__panel {
background-color: var(--pf-c-drawer__panel--BackgroundColor);
transition-behavior: allow-discrete;
gap: var(--pf-global--spacer--sm);
@media (width > 768px) {
flex-flow: row;
.pf-c-drawer__panel_content {
flex: 1 1 auto;
max-width: 33dvw;
}
}
}
}

View File

@@ -1,141 +0,0 @@
/* ----------- CSS Custom Properties for DRAWER --------------------------- */
:root {
--ak-v2-c-drawer__content--FlexBasis: 100%;
--ak-v2-c-drawer__content--BackgroundColor: var(--ak-v2-global--ContentSurface);
--ak-v2-c-drawer__content--ZIndex: var(--ak-v2-global--ZIndex--xs, auto);
--ak-v2-c-drawer__panel--MinWidth: 50%;
--ak-v2-c-drawer__panel--MaxHeight: auto;
--ak-v2-c-drawer__panel--ZIndex: var(--ak-v2-global--ZIndex--sm);
--ak-v2-c-drawer__panel--BackgroundColor: var(--ak-v2-global--ContentSurface);
--ak-v2-c-drawer__panel--TransitionDuration: var(--ak-v2-global--TransitionDuration);
--ak-v2-c-drawer__panel--TransitionProperty: margin, transform, box-shadow, flex-basis;
--ak-v2-c-drawer__panel--FlexBasis: 100%;
--ak-v2-c-drawer__panel--md--FlexBasis--min: 1.5rem;
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
--ak-v2-c-drawer__panel--md--FlexBasis--max: 100%;
--ak-v2-c-drawer__panel--xl--MinWidth: 28.125rem;
--ak-v2-c-drawer__panel--xl--FlexBasis: 28.125rem;
--ak-v2-c-drawer--m-panel-bottom__panel--md--MinHeight: 50%;
--ak-v2-c-drawer--m-panel-bottom__panel--xl--MinHeight: 18.75rem;
--ak-v2-c-drawer--m-panel-bottom__panel--xl--FlexBasis: 18.75rem;
--ak-v2-c-drawer__panel--m-resizable--FlexDirection: row;
--ak-v2-c-drawer__panel--m-resizable--md--FlexBasis--min: var(
--ak-v2-c-drawer__splitter--m-vertical--Width
);
--ak-v2-c-drawer__panel--m-resizable--MinWidth: 1.5rem;
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--FlexDirection: column;
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--md--FlexBasis--min: 1.5rem;
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--MinHeight: 1.5rem;
--ak-v2-c-drawer__splitter--Height: 0.5625rem;
--ak-v2-c-drawer__splitter--Width: 100%;
--ak-v2-c-drawer__splitter--BackgroundColor: var(--ak-v2-global--ContentSurface);
--ak-v2-c-drawer__splitter--Cursor: row-resize;
--ak-v2-c-drawer__splitter--m-vertical--Height: 100%;
--ak-v2-c-drawer__splitter--m-vertical--Width: 0.5625rem;
--ak-v2-c-drawer__splitter--m-vertical--Cursor: col-resize;
--ak-v2-c-drawer--m-inline__splitter--focus--OutlineOffset: -0.0625rem;
--ak-v2-c-drawer__splitter--after--BorderColor: var(--ak-v2-global--BorderColor--100);
--ak-v2-c-drawer__splitter--after--border-width--base: var(--ak-v2-global--BorderWidth--sm);
--ak-v2-c-drawer__splitter--after--BorderTopWidth: 0;
--ak-v2-c-drawer__splitter--after--BorderRightWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer__splitter--after--BorderBottomWidth: 0;
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: 0;
--ak-v2-c-drawer--m-panel-left__splitter--after--BorderLeftWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer--m-panel-bottom__splitter--after--BorderBottomWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer--m-inline__splitter--m-vertical--Width: 0.625rem;
--ak-v2-c-drawer--m-inline__splitter-handle--Left: 50%;
--ak-v2-c-drawer--m-inline__splitter--after--BorderRightWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer--m-inline__splitter--after--BorderLeftWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--Height: 0.625rem;
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter-handle--Top: 50%;
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--after--BorderTopWidth: var(
--ak-v2-c-drawer__splitter--after--border-width--base
);
--ak-v2-c-drawer__splitter-handle--Top: 50%;
--ak-v2-c-drawer__splitter-handle--Left: calc(
50% - var(--ak-v2-c-drawer__splitter--after--border-width--base)
);
--ak-v2-c-drawer--m-panel-left__splitter-handle--Left: 50%;
--ak-v2-c-drawer--m-panel-bottom__splitter-handle--Top: calc(
50% - var(--ak-v2-c-drawer__splitter--after--border-width--base)
);
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(--ak-v2-global--Color--200);
--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth: var(--ak-v2-global--BorderWidth--sm);
--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth: 0;
--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth: var(
--ak-v2-global--BorderWidth--sm
);
--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth: 0;
--ak-v2-c-drawer__splitter--hover__splitter-handle--after--BorderColor: var(
--ak-v2-global--Color--100
);
--ak-v2-c-drawer__splitter--focus__splitter-handle--after--BorderColor: var(
--ak-v2-global--Color--100
);
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderTopWidth: 0;
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderRightWidth: var(
--ak-v2-global--BorderWidth--sm
);
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderBottomWidth: 0;
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderLeftWidth: var(
--ak-v2-global--BorderWidth--sm
);
--ak-v2-c-drawer__splitter-handle--after--Width: 0.75rem;
--ak-v2-c-drawer__splitter-handle--after--Height: 0.25rem;
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Width: 0.25rem;
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Height: 0.75rem;
}
@media screen and (min-width: 1200px) {
:root {
--ak-v2-c-drawer__panel--MinWidth: var(--ak-v2-c-drawer__panel--xl--MinWidth);
}
}
:root {
--ak-v2-c-drawer__panel--BoxShadow: none;
--ak-v2-c-drawer--m-expanded--m-panel-bottom__panel--BoxShadow: var(
--ak-v2-global--BoxShadow--lg-top
);
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(--ak-v2-global--BoxShadow--lg-left);
}
:root {
--ak-v2-c-drawer--m-expanded--m-panel-left__panel--BoxShadow: var(
--ak-v2-global--BoxShadow--lg-right
);
}
:root {
--ak-v2-c-drawer__panel--after--Width: var(--ak-v2-global--BorderWidth--sm);
--ak-v2-c-drawer--m-panel-bottom__panel--after--Height: var(--ak-v2-global--BorderWidth--sm);
--ak-v2-c-drawer__panel--after--BackgroundColor: transparent;
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor: var(
--ak-v2-global--BorderColor--100
);
--ak-v2-c-drawer--m-inline__panel--PaddingLeft: var(--ak-v2-c-drawer__panel--after--Width);
--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight: var(
--ak-v2-c-drawer__panel--after--Width
);
--ak-v2-c-drawer--m-panel-bottom--m-inline__panel--PaddingTop: var(
--ak-v2-c-drawer__panel--after--Width
);
}
html[data-theme="dark"],
.ak-t-dark,
.pf-t-dark {
--ak-v2-c-drawer__panel--BackgroundColor: var(--ak-v2-global--ContentSurface);
--ak-v2-c-drawer__splitter--BackgroundColor: transparent;
}

View File

@@ -1,151 +0,0 @@
import "./ak-drawer";
import { DrawerExpandRequest } from "./ak-drawer.component";
import type { Meta, StoryObj } from "@storybook/web-components-vite";
import { html, TemplateResult } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
const toggle = (e: Event) => {
const button = e.target as HTMLButtonElement;
button.dispatchEvent(new DrawerExpandRequest());
};
const contentBlock = html`
<div style="padding: 1rem;">
<h2>Main Content</h2>
<p><button @click=${toggle}>Toggle Drawer</button></p>
<p>
This is the drawer's main: fill it by inserting slotted content without a slot name.
This is the part that stays visible most of the time.
</p>
<p>
Macaroon lollipop croissant sweet biscuit croissant chocolate cake. Cake cake pastry
soufflé pudding. Tiramisu lollipop chocolate cake toffee oat cake muffin topping tootsie
roll. Carrot cake bonbon chupa chups sugar plum fruitcake. Brownie sweet halvah oat cake
cheesecake topping chocolate. Wafer macaroon topping lollipop powder cupcake sugar plum
donut. Muffin wafer icing danish jelly-o bonbon. Powder shortbread brownie caramels
tootsie roll dragée liquorice. Cake lemon drops powder danish toffee.
</p>
</div>
`;
const panelBlock = html`
<style>
[slot="panel"] {
padding: 1rem;
background-color: var(--pf-v5-global--BackgroundColor--200, #f0f0f0);
}
</style>
<div slot="panel">
<h3>Panel Content</h3>
<p>This is the side panel. This is where you put the secondary information.</p>
<ul>
<li>
Seasonal, steamed, con panna and rich ut aged cup decaffeinated single origin con
panna bar
</li>
<li>Skinny mazagran whipped, black iced beans carajillo eu cream</li>
<li>Americano pumpkin spice milk ristretto caffeine single shot</li>
</ul>
<p><button @click=${toggle}>Toggle Drawer</button></p>
</div>
`;
interface DrawerProps {
expanded?: boolean;
inline?: boolean;
static?: boolean;
resizable?: boolean;
width?: string;
position?: string;
content?: TemplateResult;
panel?: TemplateResult;
}
const meta = {
title: "Components/Drawer",
component: "ak-drawer",
tags: ["autodocs"],
decorators: [
(story) =>
html`<div style="min-height: 400px; border: 1px solid #d2d2d2; overflow: hidden;">
${story()}
</div>`,
],
argTypes: {
expanded: { control: "boolean" },
position: {
control: { type: "select" },
options: ["right", "left", "bottom"],
},
inline: { control: "boolean" },
static: { control: "boolean" },
resizable: { control: "boolean" },
width: {
control: { type: "select" },
options: ["25", "33", "50", "66", "75", "100"],
},
},
} satisfies Meta;
export default meta;
type Story = StoryObj;
const Template: Story = {
args: {
expanded: false,
inline: false,
static: false,
resizable: false,
width: undefined,
position: undefined,
content: contentBlock,
panel: panelBlock,
},
render: (args) => {
return html` <ak-drawer
?expanded=${args.expanded}
?inline=${args.inline}
?resizable=${args.resizable}
position=${ifDefined(args.position)}
width=${ifDefined(args.width)}
>
${args.content} ${args.panel}
</ak-drawer>`;
},
};
export const Default: Story = {
render: () => html` <ak-drawer> ${contentBlock} ${panelBlock} </ak-drawer> `,
};
export const story = (args: DrawerProps = {}, name?: string): Story => ({
...Template,
...(name ? { name } : {}),
args: {
...Template.args,
...args,
},
});
export const Expanded: Story = story({ expanded: true });
export const PanelLeft: Story = story({ expanded: true, position: "left" });
export const PanelBottom = story({ expanded: true, position: "bottom" });
export const Inline = story({ expanded: true, inline: true });
export const Static = story({ expanded: true, static: true });
export const Resizable = story({ expanded: true, resizable: true });
export const ResizableLeft = story({ expanded: true, resizable: true, position: "left" });
export const ResizableBottom = story({ expanded: true, resizable: true, position: "bottom" });
export const CustomWidth = story({ expanded: true, width: "33" });
export const ResponsiveWidth = story({ expanded: true, width: "75-on-xl" });

View File

@@ -1,914 +0,0 @@
import { css } from "lit";
export const styles = css`
:host {
display: flex;
flex-direction: column;
height: 100%;
}
.ak-v2-c-drawer {
display: flex;
flex-direction: column;
height: 100%;
overflow-x: hidden;
}
:host([position="bottom"]) .ak-v2-c-drawer {
overflow-x: auto;
overflow-y: hidden;
}
slot {
display: contents;
}
:host([inline]:not([no-border])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([inline]:not([resizable])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static]:not([no-border])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static]:not([resizable])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
padding-inline-start: var(--ak-v2-c-drawer--m-inline__panel--PaddingLeft);
}
:host([position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
order: 0;
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([position="left"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
order: 1;
}
:host([position="bottom"]) .ak-v2-c-drawer__main {
flex-direction: column;
}
:host(:not([inline], [static])) .ak-v2-c-drawer__main {
position: relative;
}
:host(:not([inline], [static])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
position: absolute;
inset-block-start: 0;
inset-block-end: 0;
inset-inline-end: 0;
max-width: var(--ak-v2-c-drawer__panel--FlexBasis);
transform: translateX(100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host(:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([expanded]:not([inline], [static])) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([position="left"]:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
inset-inline-end: auto;
inset-inline-start: 0;
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([position="left"]:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([expanded][position="left"]:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([position="bottom"]:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
inset-inline-end: 0;
inset-inline-start: 0;
inset-block-start: auto;
inset-block-end: 0;
max-width: none;
max-height: var(--ak-v2-c-drawer__panel--FlexBasis);
transform: translateY(100%);
}
:host([position="bottom"][expanded]:not([inline], [static]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateY(0);
}
:host([class*="pf-m-resizing"]) {
--ak-v2-c-drawer__panel--TransitionProperty: none;
pointer-events: none;
}
:host([class*="pf-m-resizing"]) .ak-v2-c-drawer__splitter {
pointer-events: auto;
}
.ak-v2-c-drawer__main {
display: flex;
flex: 1;
overflow: hidden;
}
.ak-v2-c-drawer__content,
.ak-v2-c-drawer__panel,
.ak-v2-c-drawer__panel-main {
display: flex;
flex-direction: column;
flex-shrink: 0;
overflow: auto;
--ak-v2-c-drawer__content--BackgroundColor: transparent;
}
.ak-v2-c-drawer__content {
z-index: var(--ak-v2-c-drawer__content--ZIndex);
flex-basis: var(--ak-v2-c-drawer__content--FlexBasis);
order: 0;
background-color: var(--ak-v2-c-drawer__content--BackgroundColor);
}
.ak-v2-c-drawer__panel {
position: relative;
z-index: var(--ak-v2-c-drawer__panel--ZIndex);
flex-basis: var(--ak-v2-c-drawer__panel--FlexBasis);
order: 1;
max-height: var(--ak-v2-c-drawer__panel--MaxHeight);
gap: var(--ak-v2-global--spacer--sm);
overflow: auto;
background-color: var(--ak-v2-c-drawer__panel--BackgroundColor);
box-shadow: var(--ak-v2-c-drawer__panel--BoxShadow);
transition-duration: var(--ak-v2-c-drawer__panel--TransitionDuration);
transition-property: var(--ak-v2-c-drawer__panel--TransitionProperty);
transition-behavior: allow-discrete;
-webkit-overflow-scrolling: touch;
}
.ak-v2-c-drawer__panel::after {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
width: var(--ak-v2-c-drawer__panel--after--Width);
height: 100%;
content: "";
background-color: var(--ak-v2-c-drawer__panel--after--BackgroundColor);
}
@media screen and (min-width: 768px) {
.ak-v2-c-drawer__panel {
--ak-v2-c-drawer__panel--FlexBasis: max(
var(--ak-v2-c-drawer__panel--md--FlexBasis--min),
min(
var(--ak-v2-c-drawer__panel--md--FlexBasis),
var(--ak-v2-c-drawer__panel--md--FlexBasis--max)
)
);
}
}
@media screen and (min-width: 1200px) {
:host(:not([width])) .ak-v2-c-drawer__panel {
--ak-v2-c-drawer__panel--md--FlexBasis: var(--ak-v2-c-drawer__panel--xl--FlexBasis);
}
}
@media screen and (min-width: 1200px) {
:host([position="bottom"]) .ak-v2-c-drawer__panel {
--ak-v2-c-drawer__panel--md--FlexBasis: var(
--ak-v2-c-drawer--m-panel-bottom__panel--xl--FlexBasis
);
}
}
:where(
:host(:not([position])),
:host([position="left"]),
:host([position="right"]),
:host([position="start"]),
:host([position="end"])
)
.ak-v2-c-drawer__splitter {
--ak-v2-c-drawer__splitter--Height: var(--ak-v2-c-drawer__splitter--m-vertical--Height);
--ak-v2-c-drawer__splitter--Width: var(--ak-v2-c-drawer__splitter--m-vertical--Width);
--ak-v2-c-drawer__splitter--Cursor: var(--ak-v2-c-drawer__splitter--m-vertical--Cursor);
--ak-v2-c-drawer__splitter-handle--after--Width: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Width
);
--ak-v2-c-drawer__splitter-handle--after--Height: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--Height
);
--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderTopWidth
);
--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderRightWidth
);
--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderBottomWidth
);
--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth: var(
--ak-v2-c-drawer__splitter--m-vertical__splitter-handle--after--BorderLeftWidth
);
}
.ak-v2-c-drawer__splitter {
position: relative;
display: none;
width: var(--ak-v2-c-drawer__splitter--Width);
height: var(--ak-v2-c-drawer__splitter--Height);
cursor: var(--ak-v2-c-drawer__splitter--Cursor);
background-color: var(--ak-v2-c-drawer__splitter--BackgroundColor);
}
.ak-v2-c-drawer__splitter:hover {
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(
--ak-v2-c-drawer__splitter--hover__splitter-handle--after--BorderColor
);
}
.ak-v2-c-drawer__splitter:focus {
--ak-v2-c-drawer__splitter-handle--after--BorderColor: var(
--ak-v2-c-drawer__splitter--focus__splitter-handle--after--BorderColor
);
}
.ak-v2-c-drawer__splitter::after {
position: absolute;
inset-block-start: 0;
inset-block-end: 0;
inset-inline-start: 0;
inset-inline-end: 0;
content: "";
border: solid var(--ak-v2-c-drawer__splitter--after--BorderColor);
border-block-start-width: var(--ak-v2-c-drawer__splitter--after--BorderTopWidth);
border-block-end-width: var(--ak-v2-c-drawer__splitter--after--BorderBottomWidth);
border-inline-start-width: var(--ak-v2-c-drawer__splitter--after--BorderLeftWidth);
border-inline-end-width: var(--ak-v2-c-drawer__splitter--after--BorderRightWidth);
}
.ak-v2-c-drawer__splitter-handle {
position: absolute;
inset-block-start: var(--ak-v2-c-drawer__splitter-handle--Top);
inset-inline-start: var(--ak-v2-c-drawer__splitter-handle--Left);
transform: translate(-50%, -50%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"]) .ak-v2-c-drawer__splitter-handle {
transform: translate(calc(-50% * var(--ak-v2-global--inverse--multiplier)), -50%);
}
.ak-v2-c-drawer__splitter-handle::after {
display: block;
width: var(--ak-v2-c-drawer__splitter-handle--after--Width);
height: var(--ak-v2-c-drawer__splitter-handle--after--Height);
content: "";
border-color: var(--ak-v2-c-drawer__splitter-handle--after--BorderColor);
border-style: solid;
border-block-start-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderTopWidth);
border-block-end-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderBottomWidth);
border-inline-start-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderLeftWidth);
border-inline-end-width: var(--ak-v2-c-drawer__splitter-handle--after--BorderRightWidth);
}
@media screen and (min-width: 768px) {
:host {
min-width: var(--ak-v2-c-drawer__panel--MinWidth);
}
:host([expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
box-shadow: var(--ak-v2-c-drawer--m-expanded__panel--BoxShadow);
}
:host([expanded][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer__panel--md--FlexBasis--min: var(
--ak-v2-c-drawer__panel--m-resizable--md--FlexBasis--min
);
flex-direction: var(--ak-v2-c-drawer__panel--m-resizable--FlexDirection);
min-width: var(--ak-v2-c-drawer__panel--m-resizable--MinWidth);
}
:host([expanded][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
width: 0;
height: 0;
}
:host([expanded][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__splitter {
flex-shrink: 0;
}
:host([expanded][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__panel-main {
flex-shrink: 1;
}
:host([position="left"]) {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(
--ak-v2-c-drawer--m-expanded--m-panel-left__panel--BoxShadow
);
}
:host([position="left"][inline])
> .ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable),
:host([position="left"][static])
> .ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable) {
padding-inline-start: 0;
padding-inline-end: var(--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight);
}
:host([position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
inset-inline-start: auto;
inset-inline-end: 0;
}
:host([position="left"][expanded][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__splitter {
--ak-v2-c-drawer__splitter-handle--Left: var(
--ak-v2-c-drawer--m-panel-left__splitter-handle--Left
);
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: var(
--ak-v2-c-drawer--m-panel-left__splitter--after--BorderLeftWidth
);
order: 1;
}
:host([position="bottom"]) {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: var(
--ak-v2-c-drawer--m-expanded--m-panel-bottom__panel--BoxShadow
);
--ak-v2-c-drawer__panel--MaxHeight: 100%;
--ak-v2-c-drawer__panel--FlexBasis--min: var(
--ak-v2-c-drawer--m-panel-bottom__panel--FlexBasis--min
);
min-width: auto;
min-height: var(--ak-v2-c-drawer--m-panel-bottom__panel--md--MinHeight);
}
:host([position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel::after {
inset-block-start: 0;
inset-inline-start: auto;
width: 100%;
height: var(--ak-v2-c-drawer--m-panel-bottom__panel--after--Height);
}
:host([position="bottom"][resizable]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer__panel--md--FlexBasis--min: var(
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--md--FlexBasis--min
);
--ak-v2-c-drawer__panel--m-resizable--FlexDirection: var(
--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--FlexDirection
);
--ak-v2-c-drawer__panel--m-resizable--MinWidth: 0;
min-height: var(--ak-v2-c-drawer--m-panel-bottom__panel--m-resizable--MinHeight);
}
:host([position="bottom"][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__splitter {
--ak-v2-c-drawer__splitter-handle--Top: var(
--ak-v2-c-drawer--m-panel-bottom__splitter-handle--Top
);
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
--ak-v2-c-drawer__splitter--after--BorderBottomWidth: var(
--ak-v2-c-drawer--m-panel-bottom__splitter--after--BorderBottomWidth
);
}
:host([position="left"][inline]:not([no-border], [resizable]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable),
:host([position="left"][static]:not([no-border], [resizable]))
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel:not(.pf-m-no-border, .pf-m-resizable) {
padding-inline-start: 0;
padding-inline-end: var(--ak-v2-c-drawer--m-panel-left--m-inline__panel--PaddingRight);
}
:host([inline][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__splitter {
--ak-v2-c-drawer__splitter--m-vertical--Width: var(
--ak-v2-c-drawer--m-inline__splitter--m-vertical--Width
);
--ak-v2-c-drawer__splitter-handle--Left: var(
--ak-v2-c-drawer--m-inline__splitter-handle--Left
);
--ak-v2-c-drawer__splitter--after--BorderRightWidth: var(
--ak-v2-c-drawer--m-inline__splitter--after--BorderRightWidth
);
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: var(
--ak-v2-c-drawer--m-inline__splitter--after--BorderLeftWidth
);
outline-offset: var(--ak-v2-c-drawer--m-inline__splitter--focus--OutlineOffset);
}
:host([position="bottom"][inline][resizable])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel
> .ak-v2-c-drawer__splitter {
--ak-v2-c-drawer__splitter--Height: var(
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--Height
);
--ak-v2-c-drawer__splitter-handle--Top: var(
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter-handle--Top
);
--ak-v2-c-drawer__splitter--after--BorderTopWidth: var(
--ak-v2-c-drawer--m-inline--m-panel-bottom__splitter--after--BorderTopWidth
);
--ak-v2-c-drawer__splitter--after--BorderRightWidth: 0;
--ak-v2-c-drawer__splitter--after--BorderLeftWidth: 0;
}
:host([no-panel-border]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
}
.ak-v2-c-drawer__splitter {
display: block;
}
}
@media (min-width: 768px) {
:host([width="25"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
}
:host([width="33"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
}
:host([width="50"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
}
:host([width="66"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
}
:host([width="75"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
}
:host([width="100"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
}
}
@media (min-width: 992px) {
:host([width="25-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
}
:host([width="33-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
}
:host([width="50-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
}
:host([width="66-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
}
:host([width="75-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
}
:host([width="100-on-lg"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
}
}
@media (min-width: 1200px) {
:host([width="25-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
}
:host([width="33-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
}
:host([width="50-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
}
:host([width="66-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
}
:host([width="75-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
}
:host([width="100-on-xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
}
}
@media (min-width: 1450px) {
:host([width="25-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 25%;
}
:host([width="33-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 33%;
}
:host([width="50-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 50%;
}
:host([width="66-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 66%;
}
:host([width="75-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 75%;
}
:host([width="100-on-2xl"]) {
--ak-v2-c-drawer__panel--md--FlexBasis: 100%;
}
}
@media (min-width: 768px) {
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
flex-shrink: 1;
}
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
}
:host([inline]:not([no-border])),
:host([static]:not([no-border])) {
background-color: var(
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
);
}
}
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
overflow-x: auto;
}
:host([inline]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
transform: translateX(0);
}
:host([inline][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline][position="left"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline][position="left"][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([inline][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateY(100%);
}
:host([inline][expanded][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-block-end: 0;
transform: translateY(0);
}
:host([static]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([static][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([static][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
@media (min-width: 992px) {
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
flex-shrink: 1;
}
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
}
:host([inline-on-lg]:not([no-border])),
:host([static-on-lg]:not([no-border])) {
background-color: var(
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
);
}
}
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
overflow-x: auto;
}
:host([inline-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-lg])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-lg][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
transform: translateX(0);
}
:host([inline-on-lg][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-lg][position="left"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-lg][position="left"][expanded])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([inline-on-lg][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateY(100%);
}
:host([inline-on-lg][expanded][position="bottom"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-block-end: 0;
transform: translateY(0);
}
:host([static-on-lg]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([static-on-lg][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([static-on-lg][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
@media (min-width: 1200px) {
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
flex-shrink: 1;
}
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
}
:host([inline-on-xl]:not([no-border])),
:host([static-on-xl]:not([no-border])) {
background-color: var(
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
);
}
}
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
overflow-x: auto;
}
:host([inline-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-xl])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-xl][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
transform: translateX(0);
}
:host([inline-on-xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-xl][position="left"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-xl][position="left"][expanded])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([inline-on-xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateY(100%);
}
:host([inline-on-xl][expanded][position="bottom"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-block-end: 0;
transform: translateY(0);
}
:host([static-on-xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([static-on-xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([static-on-xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
@media (min-width: 1450px) {
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content,
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
flex-shrink: 1;
}
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel,
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
--ak-v2-c-drawer--m-expanded__panel--BoxShadow: none;
}
:host([inline-on-2xl]:not([no-border])),
:host([static-on-2xl]:not([no-border])) {
background-color: var(
--ak-v2-c-drawer--m-inline--m-expanded__panel--after--BackgroundColor
);
}
}
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__content {
overflow-x: auto;
}
:host([inline-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-2xl])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-2xl][expanded]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
transform: translateX(0);
}
:host([inline-on-2xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-start: 0;
margin-inline-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateX(-100%);
}
:where(.ak-v2-m-dir-rtl, [dir="rtl"])
:host([inline-on-2xl][position="left"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
transform: translateX(calc(-100% * var(--ak-v2-global--inverse--multiplier)));
}
:host([inline-on-2xl][position="left"][expanded])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([inline-on-2xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-block-end: calc(var(--ak-v2-c-drawer__panel--FlexBasis) * -1);
transform: translateY(100%);
}
:host([inline-on-2xl][expanded][position="bottom"])
.ak-v2-c-drawer__main
> .ak-v2-c-drawer__panel {
margin-block-end: 0;
transform: translateY(0);
}
:host([static-on-2xl]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
:host([static-on-2xl][position="left"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
margin-inline-end: 0;
transform: translateX(0);
}
:host([static-on-2xl][position="bottom"]) .ak-v2-c-drawer__main > .ak-v2-c-drawer__panel {
transform: translateX(0);
}
@media screen and (min-width: 1200px) {
:host([position="bottom"]) {
--ak-v2-c-drawer__panel--MinWidth: auto;
--ak-v2-c-drawer__panel--MinHeight: var(
--ak-v2-c-drawer--m-panel-bottom__panel--xl--MinHeight
);
}
}
`;
//
export default styles;

View File

@@ -1,11 +1,11 @@
import { AkDrawer } from "./ak-drawer.component.js";
import { Drawer } from "./ak-drawer.component.js";
export { AkDrawer };
export { Drawer };
window.customElements.define("ak-drawer", AkDrawer);
window.customElements.define("ak-drawer", Drawer);
declare global {
interface HTMLElementTagNameMap {
"ak-drawer": AkDrawer;
"ak-drawer": Drawer;
}
}

View File

@@ -1,195 +0,0 @@
import { type AkDrawer } from "./ak-drawer.component";
import { match, P } from "ts-pattern";
import { ReactiveController, ReactiveControllerHost } from "lit";
type DrawerResizeControllerHost = ReactiveControllerHost & AkDrawer;
type Position = "start" | "end" | "left" | "right" | "bottom";
const oneOf = P.union;
const DEFAULT_SIZE_PROPERTY_NAME = "--ak-v2-c-drawer__panel--md--FlexBasis";
const DEFAULT_RESIZE_INCREMENT = 5;
interface ResizeControllerProps {
sizeProperty?: string;
resizeIncrement?: number;
}
export class DrawerResizeController implements ReactiveController {
#abortController: AbortController | null = null;
#positions: {
start: number;
end: number;
bottom: number;
} = { start: 0, end: 0, bottom: 0 };
public resizeIncrement: number;
public sizeProperty: string;
constructor(
private host: DrawerResizeControllerHost,
props: ResizeControllerProps = {},
) {
this.resizeIncrement = props.resizeIncrement ?? DEFAULT_RESIZE_INCREMENT;
this.sizeProperty = props.sizeProperty ?? DEFAULT_SIZE_PROPERTY_NAME;
}
endController() {
this.#abortController?.abort();
this.#abortController = null;
}
restartController() {
this.endController();
this.#abortController = new AbortController();
return this.#abortController.signal;
}
hostQ(part: string): HTMLElement {
const element = this.host.renderRoot.querySelector(part);
if (element === null || !(element instanceof HTMLElement)) {
throw new Error(`Could not identify requested part ${element}`);
}
return element;
}
get drawer() {
return this.hostQ('[part="drawer"]');
}
get panel() {
return this.hostQ('[part="drawer-panel"]');
}
get content() {
return this.hostQ('[part="drawer-panel-main"]');
}
get splitter() {
return this.hostQ('[part="drawer-splitter"]');
}
get inline() {
return this.host.hasAttribute("inline");
}
get position(): Position {
return (this.host.getAttribute("position") || "end") as Position;
}
initPositions() {
const pan = this.panel.getBoundingClientRect();
this.#positions = { start: pan.left, end: pan.right, bottom: pan.bottom };
}
setResizing(resizing: boolean = true) {
if (resizing) {
this.host.setAttribute("resizing", "");
} else {
this.host.removeAttribute("resizing");
}
}
get isResizing() {
return this.host.hasAttribute("resizing");
}
handleMove(ev: MouseEvent | TouchEvent, controlPosition: number) {
ev.stopPropagation();
const newSize = match(this.position)
.with(oneOf("end", "right"), () => this.#positions.end - controlPosition)
.with(oneOf("start", "left"), () => controlPosition - this.#positions.start)
.with("bottom", () => this.#positions.bottom - controlPosition)
.otherwise(() => {
throw new Error(`Do not recognize position: ${this.position}`);
});
if (this.position === "bottom") {
this.panel.style.overflowAnchor = "none";
}
this.panel.style.setProperty(DEFAULT_SIZE_PROPERTY_NAME, `${newSize}px`);
}
handleMouseMove = (ev: MouseEvent) => {
this.handleMove(ev, this.position === "bottom" ? ev.clientY : ev.clientX);
};
handleTouchMove = (ev: TouchEvent) => {
ev.preventDefault();
ev.stopImmediatePropagation();
const touch = ev.touches[0];
this.handleMove(ev, this.position === "bottom" ? touch.clientY : touch.clientX);
};
handleMouseUp = () => {
this.setResizing(false);
this.initPositions();
this.restartController();
};
handleTouchEnd = (ev: TouchEvent) => {
ev.stopPropagation();
this.handleMouseUp();
};
handleTouchStart = (ev: TouchEvent) => {
ev.stopPropagation();
const signal = this.restartController();
document.addEventListener("touchmove", this.handleTouchMove, { passive: false, signal });
document.addEventListener("touchend", this.handleTouchEnd, { signal });
this.initPositions();
this.setResizing();
};
handleMouseDown = (ev: MouseEvent) => {
ev.stopPropagation();
ev.preventDefault();
const signal = this.restartController();
document.addEventListener("mousemove", this.handleMouseMove, { signal });
document.addEventListener("mouseup", this.handleMouseUp, { signal });
this.initPositions();
this.setResizing();
};
handleKeyDown = (ev: KeyboardEvent) => {
const key = ev.key;
const positionKeys =
this.position === "bottom" ? ["ArrowUp", "ArrowDown"] : ["ArrowLeft", "ArrowRight"];
const validKeys = ["Escape", "Enter", ...positionKeys];
// Prevent default behavior when resizing, but otherwise let it pass.
if (!validKeys.includes(key)) {
if (this.isResizing) {
ev.preventDefault();
}
return;
}
ev.preventDefault();
const delta = match([key, this.position])
.with(["ArrowRight", oneOf("end", "right")], () => -1 * this.resizeIncrement)
.with(["ArrowLeft", oneOf("end", "right")], () => this.resizeIncrement)
.with(["ArrowRight", oneOf("start", "left")], () => this.resizeIncrement)
.with(["ArrowLeft", oneOf("start", "left")], () => -1 * this.resizeIncrement)
.with(["ArrowUp", "bottom"], () => this.resizeIncrement)
.with(["ArrowDown", "bottom"], () => -1 * this.resizeIncrement)
.otherwise(() => 0);
const { height, width } = this.panel.getBoundingClientRect();
const newSize = (this.position === "bottom" ? height : width) + delta;
this.panel.style.setProperty(DEFAULT_SIZE_PROPERTY_NAME, `${newSize}px`);
};
hostConnected() {
this.host.updateComplete.then(() => {
this.initPositions();
});
}
hostDisconnected() {
this.#abortController?.abort();
this.#abortController = null;
}
}

View File

@@ -1,114 +0,0 @@
import { html, noChange, nothing, render, TemplateResult } from "lit";
import { AsyncDirective, DirectiveResult } from "lit/async-directive.js";
import { ChildPart, directive, PartInfo, PartType } from "lit/directive.js";
import { RootPart } from "lit/html.js";
export interface LightChildOptions {
// Optional alternative target for any `@event`-style handlers passed into the template. NOTE:
// this only works if the handlers do not already have a `this` bound to them, so only ordinary
// functions and methods will respond to this parameter; arrow function class fields and bound
// functions will use the `this` to which they were bound.
host?: Element;
slotName?: string;
}
class LightChildDirective extends AsyncDirective {
// These must remain public: the dependency tree of all of these leads to their being imported
// into parts of the DOM by the host, and TSC complains otherwise.
public slotName: string | null = null;
public slot: HTMLSlotElement | null = null;
public host: Element | null = null;
public rootPart: RootPart | null = null;
public sentinel: Comment | null = null;
constructor(partInfo: PartInfo) {
super(partInfo);
if (partInfo.type !== PartType.CHILD) {
throw new Error("The `light()` directive can only be use in child position");
}
}
// This is for SSR only.
render(_template?: TemplateResult | DirectiveResult, options?: LightChildOptions) {
this.slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
return html`<slot name="${this.slotName}"></slot>`;
}
update(
part: ChildPart,
[template, options = {}]: [TemplateResult | DirectiveResult, LightChildOptions],
) {
this.slotName ??= options?.slotName ?? `lc-${Math.random().toString(36).slice(2, 8)}`;
// This places a comment in the LightDom that belongs to this directive. Comments are not
// part of the DOM tree for the purposes of CSS, so it will be possible to style this child
// directly without a wrapper.
if (!this.sentinel) {
const rootNode = part.parentNode.getRootNode();
this.host ??= (part.options?.host ||
(rootNode instanceof ShadowRoot ? rootNode.host : null)) as Element | null;
if (!this.host) {
throw new Error(
"light() must be used inside a shadow root or a valid options.host",
);
}
this.sentinel = document.createComment("");
this.host.appendChild(this.sentinel);
}
if (!this.sentinel.parentNode) {
throw new Error("Could not assign sentinel to element.");
}
const renderOptions = Object.fromEntries(
Object.entries(options).filter(([key]) => ["host"].includes(key)),
);
this.rootPart = render(template, this.sentinel.parentNode as HTMLElement, {
renderBefore: this.sentinel,
...renderOptions,
});
const rendered = this.sentinel.previousSibling;
if (rendered instanceof Element) {
rendered.slot = this.slotName;
}
if (!this.slot) {
this.slot = Object.assign(document.createElement("slot"), {
name: this.slotName,
});
return this.slot;
}
return noChange;
}
disconnected() {
if (this.sentinel?.parentNode && this.host?.isConnected) {
// The node that contains the directive has been disconnected, *not* the host. We need
// to clean up the associated lightDOM element.
render(nothing, this.sentinel.parentNode as HTMLElement, {
renderBefore: this.sentinel,
});
this.sentinel.remove();
this.sentinel = null;
this.rootPart = null;
return;
}
// The host has been disconnected. Inform any child components.
this.rootPart?.setConnected(false);
}
reconnected() {
this.rootPart?.setConnected(true);
}
}
export const light = directive(LightChildDirective);

View File

@@ -80,6 +80,10 @@ export class TypeCreateWizardPage extends WithLicenseSummary(WizardPage) {
[part*="type-create"]:not(:first-child) {
margin-block-start: var(--pf-global--spacer--md);
}
:host([layout="list"]) {
justify-content: center;
}
`,
];

View File

@@ -62,23 +62,6 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
display: block;
height: min(var(--ak-c-dialog--AspectRatioHeight), var(--ak-c-dialog--MaxHeight));
}
.pf-c-wizard__main {
overscroll-behavior: contain;
display: flex;
flex-flow: column;
}
.pf-c-wizard__main,
.pf-c-wizard__main-body {
transform: translate3d(0, 0, 0);
will-change: transform;
}
.pf-c-wizard__main-body {
display: flex;
flex: 1 1 auto;
}
`,
];
@@ -521,6 +504,12 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
return html`<p>Unexpected missing step: ${step}</p>`;
}
// By default, disable steps ahead of the current step
let disabled = activeStepIndex < idx;
// If this wizard is at the end, disable navigation back
if (activeStepIndex === this.steps.length - 1 && idx !== activeStepIndex) {
disabled = true;
}
return html`<li role="presentation" class="pf-c-wizard__nav-item">
<button
class=${classMap({
@@ -528,7 +517,7 @@ export class AKWizard<S = Record<string, unknown>> extends AKElement {
"pf-m-current": idx === activeStepIndex,
})}
type="button"
?disabled=${activeStepIndex < idx}
?disabled=${disabled}
@click=${() => {
this.activeStepElement = stepEl;
}}

View File

@@ -1,11 +0,0 @@
import "@patternfly/patternfly/components/Login/login.css";
import "#stories/flow-interface";
import "#flow/stages/dummy/DummyStage";
import { flowFactory } from "#stories/flow-interface";
export default {
title: "Flow / ak-flow-executor",
};
export const BackgroundImage = flowFactory("ak-stage-dummy");

View File

@@ -1,307 +0,0 @@
import "#elements/LoadingOverlay";
import "#elements/locale/ak-locale-select";
import "#flow/inspector/FlowInspectorButton";
import "#flow/FlowExecutor";
import "#flow/tabs/broadcast";
import { FlowWebsocketClientController } from "./controllers/FlowWebsocketClientController";
import Styles from "./FlowExecutor.css" with { type: "bundled-text" };
import { globalAK } from "#common/global";
import { applyBackgroundImageProperty } from "#common/theme";
import { AKSessionAuthenticatedEvent } from "#common/ws/events";
import { listen } from "#elements/decorators/listen";
import { light } from "#elements/directives/light";
import { Interface } from "#elements/Interface";
import { WithBrandConfig } from "#elements/mixins/branding";
import { SlottedTemplateResult } from "#elements/types";
import { ThemedImage } from "#elements/utils/images";
import { AKFlowInfoUpdateEvent, AKFlowLoadingEvent } from "#flow/events";
import { ConsoleLogger } from "#logger/browser";
import { ContextualFlowInfo, FlowLayoutEnum } from "@goauthentik/api";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
/// <reference types="../../types/lit.d.ts" />
const isContextualFlowInfo = (v: unknown): v is ContextualFlowInfo =>
typeof v === "object" && v !== null;
/**
* The application shell for authentik flows and the Flow Executor.
*
* Provides the decorations and features that go around the executor: background, layout, locale
* selector, flow inspector button, headers, footers, and the iframe if provided.
*
* @attr {string} slug - The slug of the flow to execute. Prop-drilled to the executor.
* @attr {FlowLayoutEnum} data-layout - Page layout variant. Defaults to `globalAK().flow.layout` or
* just `stacked`
*
* @slot footer - The page-level footer content. Currently filled by `ak-brand-links`.
*
* @part main - The main container for the flow content.
* @part flow-executor - Wrapper around ak-flow-executor
* @part content - The container for the stage content.
* @part content-iframe - The iframe element when using a frame background layout.
* @part footer - The footer container.
* @part locale-select - The locale select component.
* @part branding - The branding element, used for the background image in some layouts.
* @part loading-overlay - The loading overlay element.
* @part locale-select-label - The label of the locale select component.
* @part locale-select-select - The select element of the locale select component.
*
* NOTE: This is the application shell, the top-level component. From here, we invoke the
* flow-executor in-line in the template rendered, but use the `light()` directive to inject it into
* the Flow element's lightDOM, and a slot is emplaced where the flow-executor's part of the
* template would go. This enables password managers to traverse down into the flow and its stages
* without having to cross or know about shadowDOM boundaries.
*
*/
@customElement("ak-flow")
export class Flow extends WithBrandConfig(Interface) {
//#region Static
public static readonly DefaultLayout: FlowLayoutEnum =
globalAK()?.flow?.layout || FlowLayoutEnum.Stacked;
static styles: CSSResult[] = [
PFLogin,
PFDrawer,
PFButton,
PFTitle,
PFList,
PFBackgroundImage,
Styles,
];
//#endregion
//#region Properties
@property()
public slug: string = window.location.pathname.split("/")[3];
// Reflection is required to trigger the correct behavior with CSS;
@property({ attribute: "data-layout", reflect: true })
public layout: FlowLayoutEnum = Flow.DefaultLayout;
@state()
protected loading = false;
@state()
public title: string = "";
@state()
public background: ContextualFlowInfo["background"];
@state()
protected backgroundThemedUrls?: ContextualFlowInfo["backgroundThemedUrls"];
#abortController: AbortController | null = null;
readonly #wsController = new FlowWebsocketClientController(this);
readonly #logger = ConsoleLogger.prefix("flow");
//#region Render
constructor() {
super();
this.addController(this.#wsController);
}
#handleFlowUpdate = (event: AKFlowInfoUpdateEvent) => {
const { flowInfo } = event;
if (!isContextualFlowInfo(flowInfo)) {
return;
}
if ("title" in flowInfo && flowInfo.title !== undefined) {
this.title = flowInfo.title;
}
if ("background" in flowInfo && flowInfo.background !== undefined) {
this.background = flowInfo.background;
}
if ("backgroundThemedUrls" in flowInfo && flowInfo.backgroundThemedUrls !== undefined) {
this.backgroundThemedUrls = flowInfo.backgroundThemedUrls;
}
if ("layout" in flowInfo && flowInfo.layout !== undefined) {
this.layout = flowInfo.layout;
}
};
#handleLoading = (event: AKFlowLoadingEvent) => {
this.loading = true;
// The event comes with a payload: a protected boolean promise that reflects the pending
// state of whatever triggered the "loading" state deep down. Neat trick here: we simply
// await on it and, when it's done, we trigger a state change. No other system needs to
// track "loading" states at all.
event.awaiter.finally(() => {
this.loading = false;
});
};
public override connectedCallback(): void {
super.connectedCallback();
if (this.#abortController) {
this.#abortController.abort();
}
this.#abortController = new AbortController();
const { signal } = this.#abortController;
this.addEventListener(AKFlowInfoUpdateEvent.eventName, this.#handleFlowUpdate, { signal });
this.addEventListener(AKFlowLoadingEvent.eventName, this.#handleLoading, { signal });
}
public override disconnectedCallback(): void {
super.disconnectedCallback();
this.#abortController?.abort();
this.#abortController = null;
}
@listen(AKSessionAuthenticatedEvent)
protected onSessionAuthenticated = () => {
if (document.hidden) {
this.#logger.debug("Reloading after session authenticated in background tab");
window.location.reload();
}
};
get #layoutUsesSidebarFrames(): boolean {
return (
this.layout === FlowLayoutEnum.SidebarLeftFrameBackground ||
this.layout === FlowLayoutEnum.SidebarRightFrameBackground
);
}
#synchronizeBackground() {
if (!(this.background || this.backgroundThemedUrls) || this.#layoutUsesSidebarFrames)
return;
const background = this.backgroundThemedUrls?.[this.activeTheme] || this.background;
// Storybook has a different document structure.
const target =
import.meta.env.AK_BUNDLER === "storybook"
? this.closest<HTMLDivElement>(".docs-story")
: this.ownerDocument.body;
applyBackgroundImageProperty(background, { target });
}
protected renderHeader() {
return ThemedImage({
src: this.brandingLogo,
alt: msg("authentik Logo"),
className: "branding-logo",
theme: this.activeTheme,
themedUrls: this.brandingLogoThemedUrls,
});
}
// Only used by the `sidebar_*_frame_backgrounds` to give customers a place to put their
// branding visuals, if they like.
//
protected renderFrameBackground() {
return guard([this.layout, this.background], () => {
if (!this.#layoutUsesSidebarFrames) return nothing;
const { background } = this;
if (!background) return nothing;
return html`
<div class="ak-c-login__content" part="content">
<iframe
class="ak-c-login__content-iframe"
part="content-iframe"
name="flow-content-frame"
src=${background}
></iframe>
</div>
`;
});
}
protected renderFooter() {
return guard([this.layout], () => {
return html`<footer
aria-label=${msg("Site footer")}
name="site-footer"
part="footer"
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
? "pf-m-dark"
: ""}"
>
<slot name="footer"></slot>
</footer>`;
});
}
protected override render(): SlottedTemplateResult {
const { loading } = this;
return html`<ak-locale-select
part="locale-select"
exportparts="label:locale-select-label,select:locale-select-select"
class="pf-m-dark"
></ak-locale-select>
<ak-flow-inspector-button></ak-flow-inspector-button>
${this.renderFrameBackground()}
<header class="pf-c-login__header"></header>
<main
data-layout=${this.layout}
class="pf-c-login__main"
aria-label=${msg("Authentication form")}
part="main"
>
<div class="pf-c-login__main-header pf-c-brand" part="branding">
${this.renderHeader()}
</div>
${loading
? html`<ak-loading-overlay part="loading-overlay"></ak-loading-overlay>`
: nothing}
<div part="flow-executor">
${light(html`<ak-flow-executor slug=${this.slug}></ak-flow-executor>`)}
</div>
</main>
${this.renderFooter()}`;
}
//#endregion
public override updated(changed: PropertyValues<this>) {
super.updated(changed);
if (changed.has("title")) {
const brand = this.brandingTitle;
document.title = this.title ? `${this.title} - ${brand}` : brand;
}
if (changed.has("activeTheme") || changed.has("background" satisfies keyof Flow)) {
this.#synchronizeBackground();
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-flow": Flow;
}
}

View File

@@ -1,24 +1,32 @@
import "#flow/stages/FlowErrorStage";
import "#elements/LoadingOverlay";
import "#elements/locale/ak-locale-select";
import "#flow/components/ak-brand-footer";
import "#flow/components/ak-flow-card";
import "#flow/inspector/FlowInspectorButton";
import "#flow/tabs/broadcast";
import { FlowIframeMessageController } from "./controllers/FlowIframeMessageController";
import { FlowMultitabController } from "./controllers/FlowMultitabController";
import { FlowWebsocketClientController } from "./controllers/FlowWebsocketClientController";
import Styles from "./FlowExecutor.css" with { type: "bundled-text" };
import { DEFAULT_CONFIG } from "#common/api/config";
import { APIError, parseAPIResponseError, pluckErrorDetail } from "#common/errors/network";
import { globalAK } from "#common/global";
import { configureSentry } from "#common/sentry/index";
import { applyBackgroundImageProperty } from "#common/theme";
import { AKSessionAuthenticatedEvent } from "#common/ws/events";
import { light } from "#elements/directives/light";
import { listen } from "#elements/decorators/listen";
import { Interface } from "#elements/Interface";
import { showAPIErrorMessage } from "#elements/messages/MessageContainer";
import { WithBrandConfig } from "#elements/mixins/branding";
import { LitPropertyRecord, SlottedTemplateResult } from "#elements/types";
import { exportParts } from "#elements/utils/attributes";
import { ThemedImage } from "#elements/utils/images";
import {
AKFlowAdvanceEvent,
AKFlowInfoUpdateEvent,
AKFlowLoadingEvent,
AKFlowSubmitRequest,
AKFlowUpdateChallengeRequest,
} from "#flow/events";
@@ -32,39 +40,32 @@ import {
ChallengeTypes,
FlowChallengeResponseRequest,
FlowErrorChallenge,
FlowLayoutEnum,
FlowsApi,
} from "@goauthentik/api";
import { spread } from "@open-wc/lit-helpers";
import { observed } from "@patternfly/pfe-core/decorators/observed.js";
import { match } from "ts-pattern";
import { match, P } from "ts-pattern";
import { html, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import { msg } from "@lit/localize";
import { CSSResult, html, nothing, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators.js";
import { guard } from "lit/directives/guard.js";
import { unsafeHTML } from "lit/directives/unsafe-html.js";
import { until } from "lit/directives/until.js";
import { html as staticHTML, unsafeStatic } from "lit/static-html.js";
import PFBackgroundImage from "@patternfly/patternfly/components/BackgroundImage/background-image.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
/// <reference types="../../types/lit.d.ts" />
type ChallengeProps = LitPropertyRecord<BaseStage<NonNullable<ChallengeTypes>, object>>;
/**
* An executor for authentik flows
*
* @remarks
*
* A *Flow* is a series of steps the authentik server takes to perform one of
* its core functions, such as authentication, enrollment, or account recovery.
* A *stage* is one step; a *challenge* is a stage that requires user input to
* complete: you username, your password, an action with an MFA.
*
* The purpose of the FlowExecutor is to receive a challenge, select the
* client-side component (also called "stages") best suited to showing the
* request to the user, send the input to the server and deal with the response.
*
* An executor for authentik flows.
*
* @attr {string} slug - The slug of the flow to execute.
* @prop {ChallengeTypes | null} challenge - The current challenge to render.
@@ -75,6 +76,7 @@ type ChallengeProps = LitPropertyRecord<BaseStage<NonNullable<ChallengeTypes>, o
* @part footer - The footer container.
* @part locale-select - The locale select component.
* @part branding - The branding element, used for the background image in some layouts.
* @part loading-overlay - The loading overlay element.
* @part challenge-additional-actions - Container in stages which have additional actions.
* @part challenge-footer-band - Container for the stage footer, used for additional actions in some stages.
* @part locale-select-label - The label of the locale select component.
@@ -82,37 +84,62 @@ type ChallengeProps = LitPropertyRecord<BaseStage<NonNullable<ChallengeTypes>, o
*/
@customElement("ak-flow-executor")
export class FlowExecutor extends WithBrandConfig(Interface) implements StageHost {
public static readonly DefaultLayout: FlowLayoutEnum =
globalAK()?.flow?.layout || FlowLayoutEnum.Stacked;
//#region Styles
static styles: CSSResult[] = [
PFLogin,
PFDrawer,
PFButton,
PFTitle,
PFList,
PFBackgroundImage,
Styles,
];
//#endregion
//#region Properties
@property({ type: String, attribute: "slug", useDefault: true })
public flowSlug: string = window.location.pathname.split("/")[3];
// A new challenge can contain data that clients may want to use to alter
// the look of the executor's container. Provide notice of those changes.
@observed("handleFlowUpdate")
@property({ attribute: false })
public challenge: ChallengeTypes | null = null;
@state()
@property({ type: Boolean })
public loading = false;
@property({ type: String, attribute: "data-layout", useDefault: true, reflect: true })
public layout: FlowLayoutEnum = FlowExecutor.DefaultLayout;
//#endregion
//#region Internal State
readonly #logger = ConsoleLogger.prefix("flow-executor");
#logger = ConsoleLogger.prefix("flow-executor");
readonly #api: FlowsApi;
#api: FlowsApi;
// Listen for challenge-forwarding events from iframe-based third-party verifiers (Device Compliance)
readonly #flowIframeMessageController = new FlowIframeMessageController(this);
#flowIframeMessageController = new FlowIframeMessageController(this);
// Listen for authentik state-change events from other tabs
readonly #flowMultitabController = new FlowMultitabController(this);
#flowMultitabController = new FlowMultitabController(this);
// Listen for server-side events and forward them to the notification handler
#flowWebsocketClientController = new FlowWebsocketClientController(this);
//#endregion
//#region Accessors
public get flowInfo() {
return this.challenge?.flowInfo ?? null;
}
//region Live event handlers
handleChallengeRequest = (event: AKFlowUpdateChallengeRequest) => {
@@ -125,10 +152,6 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
this.submit(payload, options);
};
handleFlowUpdate() {
this.dispatchEvent(new AKFlowInfoUpdateEvent(this.challenge?.flowInfo));
}
//endregion
//#region Lifecycle
@@ -139,10 +162,48 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
this.#api = new FlowsApi(DEFAULT_CONFIG);
this.addController(this.#flowIframeMessageController);
this.addController(this.#flowMultitabController);
this.addController(this.#flowWebsocketClientController);
this.addEventListener(AKFlowUpdateChallengeRequest.eventName, this.handleChallengeRequest);
this.addEventListener(AKFlowSubmitRequest.eventName, this.handleSubordinateSubmit);
}
/**
* Synchronize flow info such as background image with the current state.
*/
get #layoutUsesSidebarFrames() {
return (
this.layout === FlowLayoutEnum.SidebarLeftFrameBackground ||
this.layout === FlowLayoutEnum.SidebarRightFrameBackground
);
}
#synchronizeFlowInfo() {
if (!this.flowInfo || this.#layoutUsesSidebarFrames) return;
const background =
this.flowInfo.backgroundThemedUrls?.[this.activeTheme] || this.flowInfo.background;
// Storybook has a different document structure, so we need to adjust the target accordingly.
const target =
import.meta.env.AK_BUNDLER === "storybook"
? this.closest<HTMLDivElement>(".docs-story")
: this.ownerDocument.body;
applyBackgroundImageProperty(background, { target });
}
//#region Listeners
@listen(AKSessionAuthenticatedEvent, { target: window })
protected sessionAuthenticatedListener = () => {
if (!document.hidden) {
return;
}
console.debug("authentik/ws: Reloading after session authenticated event");
window.location.reload();
};
private setFlowErrorChallenge(error: APIError) {
this.challenge = {
component: "ak-stage-flow-error",
@@ -157,14 +218,13 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
return Promise.resolve();
}
const fetch = this.#api.flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
});
this.loading = true;
this.dispatchEvent(new AKFlowLoadingEvent(fetch));
return fetch
return this.#api
.flowsExecutorGet({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
})
.then((challenge) => {
this.challenge = challenge;
return !!this.challenge;
@@ -174,52 +234,74 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
showAPIErrorMessage(parsedError);
this.setFlowErrorChallenge(parsedError);
return false;
})
.finally(() => {
this.loading = false;
});
};
public async firstUpdated(changed: PropertyValues<this>): Promise<void> {
super.firstUpdated(changed);
this.refresh().then(() => {
window.dispatchEvent(new AKFlowAdvanceEvent());
});
}
// DOM post-processing has to happen after the render.
public updated(changedProperties: PropertyValues<this>) {
super.updated(changedProperties);
document.title = match(this.challenge?.flowInfo?.title)
.with(P.nullish, () => this.brandingTitle)
.otherwise((title) => `${title} - ${this.brandingTitle}`);
if (changedProperties.has("challenge") && this.challenge?.flowInfo) {
this.layout = this.challenge?.flowInfo?.layout || FlowExecutor.DefaultLayout;
}
if (changedProperties.has("flowInfo") || changedProperties.has("activeTheme")) {
this.#synchronizeFlowInfo();
}
}
//#endregion
//#region Public Methods
public submit = async (payload?: FlowChallengeResponseRequestBody, options?: SubmitOptions) => {
public submit = async (
payload?: FlowChallengeResponseRequestBody,
options?: SubmitOptions,
): Promise<boolean> => {
if (!payload) throw new Error("No payload provided");
if (!this.challenge) throw new Error("No challenge provided");
if (!this.flowSlug) {
if (import.meta.env.AK_BUNDLER === "storybook") {
this.#logger.debug("Skipping submit flow slug check in storybook");
return true;
}
throw new Error("No flow slug provided");
}
// The `as` clauses are necessary because OpenAPI doesn't really do enums, it does records
// and unions of records. Alternatives to using `as` would require putting the type being
// submitted into the `submit` method's definition, and then modifying every stage to tell
// the executor what type is being submitted. That would be lots of code for no win; it's
// not coherent to think a stage for a request type will submit a different request type.
// (It's possible, but if that doesn't show up in testing we're in a mess anyway.
// This order is deliberate; the executor always specifies the component token.
const { component } = this.challenge as FlowChallengeResponseRequest;
const flowChallengeResponseRequest = {
...payload,
component,
component: this.challenge.component as FlowChallengeResponseRequest["component"],
} as FlowChallengeResponseRequest;
const solve = this.#api.flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest,
});
if (!options?.invisible) {
this.dispatchEvent(new AKFlowLoadingEvent(solve));
this.loading = true;
}
return solve
return this.#api
.flowsExecutorSolve({
flowSlug: this.flowSlug,
query: window.location.search.substring(1),
flowChallengeResponseRequest,
})
.then((challenge) => {
window.dispatchEvent(new AKFlowAdvanceEvent());
this.challenge = challenge;
@@ -228,30 +310,33 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
.catch((error: APIError) => {
this.setFlowErrorChallenge(error);
return false;
})
.finally(() => {
this.loading = false;
});
};
//#region Render Challenge
protected async renderChallengeSpecialCases(challenge: ChallengeTypes) {
if (challenge.component === "xak-flow-shell") {
return html`${unsafeHTML(challenge.body)}`;
}
return this.renderChallengeError(`No stage found for component: ${challenge.component}`);
}
protected async renderChallenge(challenge: ChallengeTypes) {
const stageEntry = StageMapping.registry.get(challenge.component);
// The special cases!
if (!stageEntry) {
return this.renderChallengeSpecialCases(challenge);
if (challenge.component === "xak-flow-shell") {
return html`${unsafeHTML(challenge.body)}`;
}
return this.renderChallengeError(
`No stage found for component: ${challenge.component}`,
);
}
const challengeProps: ChallengeProps = {
".challenge": challenge,
".host": this,
};
const challengeProps: LitPropertyRecord<BaseStage<NonNullable<typeof challenge>, object>> =
{
".challenge": challenge,
".host": this,
};
const litParts = {
part: "challenge",
@@ -274,47 +359,106 @@ export class FlowExecutor extends WithBrandConfig(Interface) implements StageHos
.with("standard", () => ({ ...challengeProps, ...litParts }))
.exhaustive(),
);
return light(staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`);
return staticHTML`<${unsafeStatic(tag)} ${props}></${unsafeStatic(tag)}>`;
}
protected renderChallengeError(error: unknown): SlottedTemplateResult {
const detail = pluckErrorDetail(error);
// eslint-disable-next-line no-console
console.trace(error);
const errorChallenge: FlowErrorChallenge = {
component: "ak-stage-flow-error",
error: pluckErrorDetail(error),
error: detail,
requestId: "",
};
return html`<ak-stage-flow-error .challenge=${errorChallenge}></ak-stage-flow-error>`;
}
protected renderPlaceholder() {
return html`<slot name="placeholder"></slot>`;
}
//#endregion
//#region Render
protected renderLoading(): SlottedTemplateResult {
return html`<slot name="placeholder"></slot>`;
}
protected renderFrameBackground(): SlottedTemplateResult {
return guard([this.layout, this.challenge], () => {
if (!this.#layoutUsesSidebarFrames) return;
const src = this.challenge?.flowInfo?.background;
if (!src) return nothing;
return html`
<div class="ak-c-login__content" part="content">
<iframe
class="ak-c-login__content-iframe"
part="content-iframe"
name="flow-content-frame"
src=${src}
></iframe>
</div>
`;
});
}
protected renderFooter(): SlottedTemplateResult {
return guard([this.layout], () => {
return html`<footer
aria-label=${msg("Site footer")}
name="site-footer"
part="footer"
class="pf-c-login__footer ${this.layout === FlowLayoutEnum.Stacked
? "pf-m-dark"
: ""}"
>
<slot name="footer"></slot>
</footer>`;
});
}
protected override render(): SlottedTemplateResult {
const { challenge } = this;
return guard([challenge], () =>
challenge?.component
? until(this.renderChallenge(challenge), this.renderPlaceholder())
: this.renderPlaceholder(),
);
const { challenge, loading } = this;
return html`<ak-locale-select
part="locale-select"
exportparts="label:locale-select-label,select:locale-select-select"
class="pf-m-dark"
></ak-locale-select>
${this.renderFrameBackground()}
<header class="pf-c-login__header">
<ak-flow-inspector-button></ak-flow-inspector-button>
</header>
<main
data-layout=${this.layout}
class="pf-c-login__main"
aria-label=${msg("Authentication form")}
part="main"
>
<div class="pf-c-login__main-header pf-c-brand" part="branding">
${ThemedImage({
src: this.brandingLogo,
alt: msg("authentik Logo"),
className: "branding-logo",
theme: this.activeTheme,
themedUrls: this.brandingLogoThemedUrls,
})}
</div>
${loading && challenge ? html`<ak-loading-overlay></ak-loading-overlay>` : nothing}
${guard([challenge], () => {
return challenge?.component
? until(this.renderChallenge(challenge))
: this.renderLoading();
})}
</main>
${this.renderFooter()}`;
}
//#endregion
public override firstUpdated(changed: PropertyValues<this>) {
super.firstUpdated(changed);
this.refresh().then(() => {
window.dispatchEvent(new AKFlowAdvanceEvent());
});
}
}
declare global {

View File

@@ -1,8 +1,6 @@
import type { FlowChallengeResponseRequestBody, SubmitOptions, SubmitRequest } from "#flow/types";
import { ChallengeTypes, ContextualFlowInfo } from "@goauthentik/api";
const PROPAGATES = { bubbles: true, composed: true };
import { ChallengeTypes } from "@goauthentik/api";
/**
* @file Flow event utilities.
@@ -19,7 +17,7 @@ export class AKFlowInspectorChangeEvent extends Event {
public readonly open: boolean;
constructor(open: boolean) {
super(AKFlowInspectorChangeEvent.eventName, PROPAGATES);
super(AKFlowInspectorChangeEvent.eventName, { bubbles: true, composed: true });
this.open = open;
}
@@ -48,7 +46,7 @@ export class AKFlowAdvanceEvent extends Event {
public static readonly eventName = "ak-flow-advance";
constructor() {
super(AKFlowAdvanceEvent.eventName, PROPAGATES);
super(AKFlowAdvanceEvent.eventName, { bubbles: true, composed: true });
}
}
@@ -64,7 +62,7 @@ export class AKFlowUpdateChallengeRequest extends Event {
public challenge: ChallengeTypes;
constructor(challenge: ChallengeTypes) {
super(AKFlowUpdateChallengeRequest.eventName, PROPAGATES);
super(AKFlowUpdateChallengeRequest.eventName, { bubbles: true, composed: true });
this.challenge = challenge;
}
}
@@ -77,7 +75,7 @@ export class AKFlowSubmitRequest extends Event {
payload: FlowChallengeResponseRequestBody,
options: SubmitOptions = { invisible: false },
) {
super(AKFlowSubmitRequest.eventName, PROPAGATES);
super(AKFlowSubmitRequest.eventName, { bubbles: true, composed: true });
this.request = {
payload,
options,
@@ -85,31 +83,6 @@ export class AKFlowSubmitRequest extends Event {
}
}
export class AKFlowInfoUpdateEvent extends Event {
public static readonly eventName = "ak-flow-info-update-event";
public flowInfo: ContextualFlowInfo | null = null;
constructor(flowInfo?: ContextualFlowInfo) {
super(AKFlowInfoUpdateEvent.eventName, PROPAGATES);
this.flowInfo = flowInfo ?? null;
}
}
// This is subtle: we don't actually *care* about the Promise's payload; we only care to show some
// "loading" message (spinner, skeleton, whatever) when there's a network transaction underway. So
// when we start a transaction, we send a copy of its promise in an event; upon receipt, a listener
// can show whatever visual effect is desired, then listen for the promise to resolve, then stop the
// visual effect. Complete separation and independence.
//
export class AKFlowLoadingEvent extends Event {
public static readonly eventName = "ak-flow-loading-event";
public awaiter: Promise<unknown>;
constructor(awaiter: Promise<unknown>) {
super(AKFlowLoadingEvent.eventName, PROPAGATES);
this.awaiter = awaiter;
}
}
//#endregion
declare global {
@@ -120,8 +93,6 @@ declare global {
interface HTMLElementEventMap {
[AKFlowSubmitRequest.eventName]: AKFlowSubmitRequest;
[AKFlowInfoUpdateEvent.eventName]: AKFlowInfoUpdateEvent;
[AKFlowLoadingEvent.eventName]: AKFlowLoadingEvent;
[AKFlowUpdateChallengeRequest.eventName]: AKFlowUpdateChallengeRequest;
}
}

View File

@@ -1,7 +1,6 @@
import "#elements/messages/MessageContainer";
import "#elements/ak-drawer/ak-drawer";
import "#flow/Flow";
import "#flow/components/ak-brand-links";
import "#flow/FlowExecutor";
// Statically import some stages to speed up load speed
import "#flow/stages/access_denied/AccessDeniedStage";
// Import webauthn-related stages to prevent issues on safari

View File

@@ -73,9 +73,9 @@ export class FlowInspectorButton extends WithCapabilitiesConfig(AKElement) {
const drawer = document.getElementById("flow-drawer");
if (changed.has("open") && drawer) {
if (this.open) {
drawer.setAttribute("expanded", "");
drawer.setAttribute("open", "");
} else {
drawer.removeAttribute("expanded");
drawer.removeAttribute("open");
}
}
}

View File

@@ -4,7 +4,6 @@ import "#flow/components/ak-flow-card";
import "#flow/components/ak-flow-password-input";
import "#flow/stages/captcha/CaptchaStage";
import { light } from "#elements/directives/light";
import { renderSourceIcon } from "#elements/sources/utils";
import { AKFormErrors } from "#components/ak-field-errors";
@@ -96,6 +95,8 @@ export class IdentificationStage extends BaseStage<
protected passwordFieldRef = createRef<HTMLInputElement>();
#form?: HTMLFormElement;
public defaultUserIdentification: string | null = null;
protected rememberMeController: RememberMeController | null = null;
@@ -128,6 +129,8 @@ export class IdentificationStage extends BaseStage<
this.#prepareRememberMeFrame = requestAnimationFrame(() => {
this.prepareRememberMeController();
});
this.#createHelperForm();
}
}
@@ -178,6 +181,101 @@ export class IdentificationStage extends BaseStage<
//#endregion
//#region Helper Form
#createHelperForm(): void {
const compatMode = "ShadyDOM" in window;
this.#form = document.createElement("form");
document.documentElement.appendChild(this.#form);
// Only add the additional username input if we're in a shadow dom
// otherwise it just confuses browsers
if (!compatMode) {
// This is a workaround for the fact that we're in a shadow dom
// adapted from https://github.com/home-assistant/frontend/issues/3133
const username = document.createElement("input");
username.setAttribute("type", "text");
username.setAttribute("name", "username"); // username as name for high compatibility
username.setAttribute("autocomplete", "username");
username.onkeyup = (ev: Event) => {
const el = ev.target as HTMLInputElement;
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
input.value = el.value;
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
this.#form.appendChild(username);
}
// Only add the password field when we don't already show a password field
if (!compatMode && !this.challenge?.passwordFields) {
const password = document.createElement("input");
password.setAttribute("type", "password");
password.setAttribute("name", "password");
password.setAttribute("autocomplete", "current-password");
password.onkeyup = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
this.submitForm();
}
const el = event.target as HTMLInputElement;
// Because the password field is not actually on this page,
// and we want to 'prefill' the password for the user,
// save it globally
PasswordManagerPrefill.password = el.value;
// Because password managers fill username, then password,
// we need to re-focus the uid_field here too
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
this.#form.appendChild(password);
}
const totp = document.createElement("input");
totp.setAttribute("type", "text");
totp.setAttribute("name", "code");
totp.setAttribute("autocomplete", "one-time-code");
totp.onkeyup = (event: KeyboardEvent) => {
if (event.key === "Enter") {
event.preventDefault();
this.submitForm();
}
const el = event.target as HTMLInputElement;
// Because the totp field is not actually on this page,
// and we want to 'prefill' the totp for the user,
// save it globally
PasswordManagerPrefill.totp = el.value;
// Because totp managers fill username, then password, then optionally,
// we need to re-focus the uid_field here too
(this.shadowRoot || this)
.querySelectorAll<HTMLInputElement>("input[name=uidField]")
.forEach((input) => {
// Because we assume only one input field exists that matches this
// call focus so the user can press enter
input.focus();
});
};
this.#form.appendChild(totp);
}
//#endregion
protected override onSubmitSuccess(): void {
this.#form?.remove();
}
protected override onSubmitFailure(): void {
this.#captcha.onFailure();
}
@@ -259,7 +357,7 @@ export class IdentificationStage extends BaseStage<
// prettier-ignore
return html`${offerRecovery ? this.renderRecoveryMessage() : nothing}
<div class="pf-c-form__group">
<div class="pf-c-form__group">
${AKLabel({ required: true, htmlFor: inputID }, label)}
${this.renderUidField(inputID, type, label, initialUserIdentification, passwordFields)}
${rememberMeController?.renderToggleInput() ?? null}
@@ -352,16 +450,14 @@ export class IdentificationStage extends BaseStage<
protected renderIdentificationStage(challenge: IdentificationChallenge) {
const { applicationPre, passwordlessUrl, showSourceLabels, sources = [] } = challenge;
return html` <div>
${light(
html`<form class="pf-c-form" @submit=${this.submitForm}>
${applicationPre ? this.renderPrelude(applicationPre) : nothing}
${this.renderInput(challenge)}
${passwordlessUrl ? this.renderPasswordlessUrl(passwordlessUrl) : nothing}
</form>`,
)}
return html`
<form class="pf-c-form" @submit=${this.submitForm}>
${applicationPre ? this.renderPrelude(applicationPre) : nothing}
${this.renderInput(challenge)}
${passwordlessUrl ? this.renderPasswordlessUrl(passwordlessUrl) : nothing}
</form>
${sources.length ? this.renderLoginSources(sources, showSourceLabels) : nothing}
</div>`;
`;
}
protected renderFooter({ enrollUrl, recoveryUrl }: IdentificationFooter) {
@@ -378,23 +474,15 @@ export class IdentificationStage extends BaseStage<
<legend class="sr-only">${msg("Additional actions")}</legend>
${enrollUrl
? html`<div class="pf-c-login__main-footer-band-item">
${light(
html`<span
>${msg("Need an account?")}
<a href="${enrollUrl}" data-ouia-component-id="enroll"
>${msg("Sign up.")}</a
></span
>`,
)}
${msg("Need an account?")}
<a href="${enrollUrl}" data-ouia-component-id="enroll">${msg("Sign up.")}</a>
</div>`
: nothing}
${recoveryUrl
? html`<div class="pf-c-login__main-footer-band-item">
${light(
html`<a href="${recoveryUrl}" data-ouia-component-id="recovery"
>${msg("Forgot username or password?")}</a
>`,
)}
<a href="${recoveryUrl}" data-ouia-component-id="recovery"
>${msg("Forgot username or password?")}</a
>
</div>`
: nothing}
</fieldset>`;

View File

@@ -44,90 +44,8 @@
--ak-sidebar--minimum-auto-width: 80rem;
}
/* #region Root globals, V2 */
:root {
/* ---- Background Colors ---- */
--ak-v2-global--BackgroundColor--100: #fff;
--ak-v2-global--BorderWidth--sm: 1px;
/* ---- Text Colors ---------- */
--pf-v5-global--Color--100: #151515;
/* ---- Border Colors -------- */
--ak-v2-global--BorderColor--100: #d2d2d2;
--ak-v2-global--BorderColor--200: #8a8d90;
/* ---- Box Shadows ------ */
--ak-v2-global--BoxShadow--lg:
0 0.5rem 1rem 0 rgba(3, 3, 3, 0.16), 0 0 0.375rem 0 rgba(3, 3, 3, 0.08);
--ak-v2-global--BoxShadow--lg-top: 0 -0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
--ak-v2-global--BoxShadow--lg-right: 0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
--ak-v2-global--BoxShadow--lg-bottom: 0 0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
--ak-v2-global--BoxShadow--lg-left: -0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.18);
/* ---- Spacers -------------- */
--ak-v2-global--spacer--xs: 0.25rem;
--ak-v2-global--spacer--sm: 0.5rem;
--ak-v2-global--spacer--md: 1rem;
--ak-v2-global--spacer--lg: 1.5rem;
--ak-v2-global--spacer--xl: 2rem;
--ak-v2-global--spacer--2xl: 3rem;
--ak-v2-global--spacer--3xl: 4rem;
--ak-v2-global--spacer--4xl: 5rem;
--ak-v2-global--spacer--form-element: 0.375rem;
--ak-v2-global--gutter: 1rem;
--ak-v2-global--gutter--md: 1.5rem;
/* ---- Z-Index -------------- */
--ak-v2-global--ZIndex--xs: 100;
--ak-v2-global--ZIndex--sm: 200;
/* ---- Animation ------------ */
--ak-v2-global--TransitionDuration: 250ms;
/* ---- Customization Bridge - */
--ak-v2-global--dark-background: var(--ak-dark-background);
}
/* -------- Dark Theme ------------------------------- */
[data-theme="dark"] {
/* ---- Background Colors ---- */
--ak-v2-global--BackgroundColor--100: #18191a;
/* ---- Text Colors ---------- */
--ak-v2-global--Color--100: #e0e0e0;
/* ---- Border Colors -------- */
--ak-v2-global--BorderColor--100: #444548;
--ak-v2-global--BorderColor--200: #444548;
/* ---- Box Shadows ------ */
--ak-v2-global--BoxShadow--lg:
0 0.5rem 1rem 0 rgba(3, 3, 3, 0.64), 0 0 0.375rem 0 rgba(3, 3, 3, 0.32);
--ak-v2-global--BoxShadow--lg-top: 0 -0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
--ak-v2-global--BoxShadow--lg-right: 0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
--ak-v2-global--BoxShadow--lg-bottom: 0 0.75rem 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
--ak-v2-global--BoxShadow--lg-left: -0.75rem 0 0.75rem -0.5rem rgba(3, 3, 3, 0.72);
}
/* -------- Semantic Names -------------------------- */
:root {
/* ---- Background Colors ---- */
--ak-v2-global--ContentSurface: var(--ak-v2-global--BackgroundColor--100);
--ak-v2-global--SecondaryContentSurface: var(--ak-v2-global--BackgroundColor--200);
/* Not sure what to call this next one; this is the background color Patternfly uses when you hover
over something and it changes color to indicate it's interactive in some way. It's the same
color as the one above in their default theme. */
--ak-v2-global--AffordanceIndicatedSurface: var(--ak-v2-global--BackgroundColor--200);
/* ---- Text Colors ---- */
--ak-v2-global--PrimaryText: var(--ak-v2-global--Color--100);
/* ---- Border Colors ---- */
--ak-v2-global--StandardBorder: var(--pf-v5-global--BorderColor--100);
--ak-v2-global--InputAccentBorder: var(--pf-v5-global--BorderColor--200);
html[data-theme="dark"] {
--ak-global--BackgroundColorContrast--100: var(--pf-global--palette--black-150);
}
/* #endregion */

View File

@@ -8,7 +8,7 @@
--pf-c-modal-box__header--PaddingTop: var(--ak-c-modal-box__header--BlockSpacer);
--ak-c-modal-box__footer--BlockSpacer: clamp(0.25em, var(--pf-global--spacer--xl), 3cqb);
--ak-c-modal-box__footer--BlockSpacer: clamp(0.25em, var(--pf-global--spacer--xl), 2cqb);
--pf-c-modal-box__footer--PaddingTop: var(--ak-c-modal-box__footer--BlockSpacer);
--pf-c-modal-box__footer--PaddingBottom: var(--ak-c-modal-box__footer--BlockSpacer);
}

View File

@@ -10,26 +10,44 @@
--pf-c-wizard__close--Right: var(--ak-c-wizard__header--InlineSpacer);
--pf-c-wizard__close--Top: var(--ak-c-wizard__header--BlockSpacer);
--ak-c-wizard__footer--BlockSpacer: clamp(0.25em, var(--pf-global--spacer--xl), 3cqb);
--ak-c-wizard__footer--BlockSpacer: clamp(0.25em, var(--pf-global--spacer--xl), 2cqb);
--pf-c-wizard__footer--PaddingTop: var(--ak-c-wizard__footer--BlockSpacer);
--pf-c-wizard__footer--PaddingBottom: var(--ak-c-wizard__footer--BlockSpacer);
--pf-c-wizard__footer--child--MarginBottom: 0;
}
.pf-c-wizard__main {
overscroll-behavior: contain;
display: flex;
flex-flow: column;
height: min(var(--ak-c-dialog--MaxHeight), 100cqi);
}
.pf-c-wizard__main-body {
--ak-c-fieldset--BorderColor: var(--pf-global--BackgroundColor--150);
display: flex;
flex: 1 1 auto;
gap: var(--pf-global--spacer--lg);
fieldset {
.pf-c-description-list {
margin-inline: var(--pf-global--spacer--sm);
}
.ak-c-fieldset .pf-c-description-list {
margin-inline: var(--pf-global--spacer--sm);
}
& > .pf-c-form {
place-content: start;
}
}
.pf-c-wizard__main,
.pf-c-wizard__main-body {
transform: translate3d(0, 0, 0);
will-change: transform;
}
.pf-c-wizard__main-title {
width: 100%;
flex: 0 0 auto;
font-family: var(--pf-global--FontFamily--heading--sans-serif);
font-size: var(--pf-global--FontSize--md);
font-weight: var(--pf-global--FontWeight--bold);

View File

@@ -1,16 +1,10 @@
@import "@patternfly/patternfly/base/patternfly-common.css";
@import "@patternfly/patternfly/base/patternfly-globals.css";
@import "@patternfly/patternfly/base/patternfly-variables.css";
@import "@patternfly/patternfly/base/patternfly-themes.css";
@import "@patternfly/patternfly/base/patternfly-fa-icons.css";
@import "@patternfly/patternfly/base/patternfly-pf-icons.css";
@import "@patternfly/patternfly/components/Spinner/spinner.css";
@import "@patternfly/patternfly/components/InputGroup/input-group.css";
@import "@patternfly/patternfly/components/FormControl/form-control.css";
@import "@patternfly/patternfly/components/Form/form.css";
@import "@patternfly/patternfly/components/Button/button.css";
@import "@patternfly/patternfly/components/Login/login.css";
@import "#fonts/RedHat/faces.css";
@@ -20,7 +14,6 @@
@import "./base/globals.css";
@import "./base/common.css";
@import "./base/placeholder.css";
@import "#elements/ak-drawer/ak-drawer.root.css";
@import "#styles/locales/ja/globals.css";
@import "#styles/locales/ko/globals.css";
@@ -30,8 +23,6 @@
@import "#elements/locale/ak-locale-select.css";
@import "#elements/locale/ak-locale-select.css";
@import "#flow/FlowExecutor.css";
@import "#flow/stages/identification/styles.css";
@import "./components/Form/form.css";
/**
* @file Static global styles for authentik.

View File

@@ -12,7 +12,6 @@
@import "./components/Fieldset/fieldset.css";
@import "./components/Login/login.css";
@import "./components/Icon/icon.css";
@import "#elements/ak-drawer/ak-drawer.root.css";
@import "#elements/locale/ak-locale-select.css";
@import "#elements/locale/ak-locale-select.css";
@import "#flow/FlowExecutor.css";

View File

@@ -1,21 +0,0 @@
/** @type { import("stylelint").Config } */
export default {
extends: "stylelint-config-standard",
rules: {
"custom-property-pattern": [
"^([A-Za-z][A-Za-z0-9]*)((__|--?)[A-Za-z0-9]+)*$",
{
message: "Expected custom property name to be kebab-case",
},
],
"selector-class-pattern": [
"^([a-z][a-z0-9]*)((__?|-)[A-Za-z0-9]+)*$",
{
message: (/** @type {string} */ selector) =>
`Expected class selector "${selector}" to be kebab-case`,
},
],
"declaration-empty-line-before": null,
"media-feature-range-notation": null,
},
};

View File

@@ -25,6 +25,7 @@ Before you begin, ensure you have the following tools installed. You can run the
- [Docker](https://www.docker.com/) (Latest Community Edition or Docker Desktop)
- [Docker Compose](https://docs.docker.com/compose/) (Compose v2)
- [Make](https://www.gnu.org/software/make/) (3 or later)
- [CMake](https://cmake.org/) (Latest stable release)
### Understanding the architecture
@@ -66,7 +67,8 @@ uv \
postgresql \
node@24 \
golangci-lint \
krb5
krb5 \
cmake
```
</TabItem>
@@ -85,7 +87,8 @@ libxslt1-dev \
libxmlsec1 \
xmlsec1 \
postgresql-server-dev-all \
postgresql
postgresql \
cmake
```
For other distributions (Red Hat, SUSE, Arch), adjust the package names as needed.

View File

@@ -4,7 +4,7 @@ sidebar_label: AFFiNE
support_level: community
---
## What is AFFiNE
## What is AFFiNE?
> AFFiNE is an open-source platform that allows you to bring together documents, whiteboards, and databases. It is a reliable tool designed to create a professional workspace for your work. With AFFiNE, you can focus on practicality and efficiency, making it easier to collaborate on your projects.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is ChatGPT
## What is ChatGPT?
> ChatGPT is OpenAI's conversational AI platform that provides chat-based assistance across the web and desktop applications.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: HedgeDoc
support_level: community
---
## What is HedgeDoc
## What is HedgeDoc?
> HedgeDoc lets you create real-time collaborative markdown notes.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Joplin
support_level: community
---
## What is Joplin Server
## What is Joplin Server?
> Joplin is an open source note-taking app. Capture your thoughts and securely access them from any device.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Kanboard
support_level: community
---
## What is Kanboard
## What is Kanboard?
> Kanboard is a free and open source Kanban project management software.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Kimai
support_level: community
---
## What is Kimai
## What is Kimai?
> Kimai is a free & open source timetracker. It tracks work time and prints out a summary of your activities on demand. Yearly, monthly, daily, by customer, by project … Its simplicity is its strength. Due to Kimai's browser based interface it runs cross-platform, even on your mobile device.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: mailcow
support_level: community
---
## What is mailcow
## What is mailcow?
> mailcow is a Dockerized, open-source groupware and email suite based on Docker. It relies on many well-known and long-used components, which, when combined, result in a comprehensive email server solution.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Mastodon
support_level: community
---
## What is Mastodon
## What is Mastodon?
> Mastodon is free and open-source software for running self-hosted social networking services. It has microblogging features similar to Twitter
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Matrix Synapse
support_level: community
---
## What is Matrix Synapse
## What is Matrix Synapse?
> Matrix is an open source project that publishes the Matrix open standard for secure, decentralized, real-time communication, and its Apache licensed reference implementations.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is Mattermost Team Edition
## What is Mattermost Team Edition?
> Mattermost is an open source, real-time collaboration platform. It provides chat, audio/video calling, screen sharing, and a plugin architecture for extending its capabilities. Mattermost Team Edition is the free, open-source version of the product.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Mautic
support_level: community
---
## What is Mautic
## What is Mautic?
> Mautic provides free and open source marketing automation software available to everyone. Free email marketing and lead management software.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Mobilizon
support_level: community
---
## What is Mobilizon
## What is Mobilizon?
> Gather, organize and mobilize yourselves with a convivial, ethical, and emancipating tool. https://joinmobilizon.org
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Nextcloud
support_level: community
---
## What is Nextcloud
## What is Nextcloud?
> Nextcloud is a suite of client-server software for creating and using file hosting services. Nextcloud is free and open-source, which means that anyone is allowed to install and operate it on their own private server devices.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: OnlyOffice
support_level: community
---
## What is OnlyOffice
## What is OnlyOffice?
> OnlyOffice, stylized as ONLYOFFICE, is a free software office suite developed by Ascensio System SIA, a company headquartered in Riga, Latvia. It features online document editors, platform for document management, corporate communication, mail and project management tools
>

View File

@@ -4,7 +4,7 @@ sidebar_label: OpenProject
support_level: community
---
## What is OpenProject
## What is OpenProject?
> OpenProject is a web-based project management software. Use OpenProject to manage your projects, tasks and goals. Collaborate via work packages and link them to your pull requests on Github.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: ownCloud
support_level: community
---
## What is ownCloud
## What is ownCloud?
> ownCloud is a free and open-source software project for content collaboration and sharing and syncing of files.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Placetel
support_level: community
---
## What is Placetel
## What is Placetel?
> Placetel is a German cloud communications provider, specializing in VoIP-based telephony, unified communications (UCaaS), and collaboration tools for businesses.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Planka
support_level: community
---
## What is Planka
## What is Planka?
> Planka is an open-source, Trello-like application with a Kanban board system, used for project management.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Rocket.chat
support_level: community
---
## What is Rocket.chat
## What is Rocket.chat?
> Rocket.Chat is an open-source fully customizable communications platform developed in JavaScript for organizations with high standards of data protection. It is licensed under the MIT License with some other licenses mixed in. See [Rocket.chat GitHub](https://github.com/RocketChat/Rocket.Chat/blob/develop/LICENSE) for licensing information.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Roundcube
support_level: community
---
## What is Roundcube
## What is Roundcube?
> Roundcube is a browser-based multilingual IMAP client with an application-like user interface. It provides the full functionality you expect from an email client, including MIME support, address book, folder manipulation, message searching and spell checking.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: SeaTable
support_level: community
---
## What is SeaTable
## What is SeaTable?
> SeaTable is a no-code database and app builder platform that provides a web-based, spreadsheet-like interface for organizing data, building apps, and automating workflows. It is designed to function as a collaborative database with features like tables, views, forms, and permissions.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: SharePoint Server SE
support_level: community
---
## What is Microsoft SharePoint
## What is Microsoft SharePoint?
> SharePoint is a proprietary, web-based collaborative platform that integrates natively with Microsoft 365.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Slack
support_level: authentik
---
## What is Slack
## What is Slack?
> Slack is a platform for collaboration, with chat and real-time video capabilities. To learn more, visit https://slack.com.

View File

@@ -4,7 +4,7 @@ sidebar_label: The Lounge
support_level: community
---
## What is The Lounge
## What is The Lounge?
> The Lounge is a modern, web-based IRC (Internet Relay Chat) client that allows users to stay connected to IRC servers even when offline.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is Vikunja
## What is Vikunja?
> Vikunja is an Open-Source, self-hosted To-Do list application for all platforms.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Wekan
support_level: community
---
## What is Wekan
## What is Wekan?
> Wekan is an open-source kanban board which allows a card-based task and to-do management.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Writefreely
support_level: community
---
## What is Writefreely
## What is Writefreely?
> An open source platform for building a writing space on the web.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Zoom
support_level: community
---
## What is Zoom
## What is Zoom?
> Zoom is a video conferencing and collaboration platform. It allows users to hold online meetings, webinars, chats, and calls over the internet.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Zulip
support_level: community
---
## What is Zulip
## What is Zulip?
> Zulip is an open-source team chat application that organizes conversations into topic-based streams, enabling more structured and efficient communication compared to traditional linear chat platforms.
>

View File

@@ -7,7 +7,7 @@ support_level: authentik
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is AWS
## What is AWS?
> AWS, or Amazon Web Services, is a comprehensive cloud computing platform. It provides a wide array of on-demand IT services like computing power, storage, and databases, allowing businesses to build and run applications, and manage infrastructure through the internet.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Amazon Web Services (IAM Identity Center)
support_level: authentik
---
## What is AWS
## What is AWS?
> AWS, or Amazon Web Services, is a comprehensive cloud computing platform. It provides a wide array of on-demand IT services like computing power, storage, and databases, allowing businesses to build and run applications, and manage infrastructure through the internet.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: DigitalOcean
support_level: community
---
## What is DigitalOcean
## What is DigitalOcean?
> DigitalOcean is a cloud infrastructure provider that offers developers simple, scalable virtual servers (droplets), managed databases, and other cloud services to deploy and manage applications efficiently.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Google Workspace
support_level: authentik
---
## What is Google Workspace
## What is Google Workspace?
> Google Workspace is a collection of cloud computing, productivity and collaboration tools, software and products developed and marketed by Google.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: HashiCorp Cloud Platform
support_level: community
---
## What is HashiCorp Cloud
## What is HashiCorp Cloud?
> HashiCorp Cloud Platform is a fully managed platform for Terraform, Vault, Consul, and more.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Oracle Cloud
support_level: community
---
## What is Oracle Cloud
## What is Oracle Cloud?
> Oracle Cloud is the first public cloud built from the ground up to be a better cloud for every application. By rethinking core engineering and systems design for cloud computing, we created innovations that accelerate migrations, deliver better reliability and performance for all applications, and offer the complete services customers need to build innovative cloud applications.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: OVHcloud
support_level: community
---
## What is OVHcloud
## What is OVHcloud?
> OVHcloud is a French cloud provider. They provide public and private cloud products, shared hosting, and dedicated servers in 140 countries.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Homarr
support_level: community
---
## What is Homarr
## What is Homarr?
> A sleek, modern dashboard that puts all of your apps and services at your fingertips. Control everything in one convenient location. Seamlessly integrates with the apps you've added, providing you with valuable information.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Linkwarden
support_level: community
---
## What is Linkwarden
## What is Linkwarden?
> Linkwarden is an open-source collaborative bookmark manager used to collect, organize, and preserve webpages.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: organizr
support_level: community
---
## What is organizr
## What is organizr?
> Organizr allows you to setup "Tabs" that will be loaded all in one webpage.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Coder
support_level: community
---
## What is Coder
## What is Coder?
> Coder is an open-source platform that provides browser-based cloud development environments, enabling developers and teams to securely write, edit, and manage code remotely without the need for local setup.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: engomo
support_level: community
---
## What is engomo
## What is engomo?
> engomo is a low-code app development platform to create enterprise apps for smartphones and tablets based on Android, iOS, or iPadOS.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Forgejo
support_level: community
---
## What is Forgejo
## What is Forgejo?
> Forgejo is a lightweight, selfhosted alternative to GitHub/GitLab, with a strong emphasis on community governance and open development.
>

View File

@@ -8,7 +8,7 @@ support_level: community
These instructions apply to all projects in the Frappe Family, including ERPNext.
:::
## What is Frappe
## What is Frappe?
> Frappe is a full stack, batteries-included, web framework written in Python and JavaScript.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is GitHub Enterprise Managed Users
## What is GitHub Enterprise Managed Users?
> With Enterprise Managed Users, you manage the lifecycle and authentication of your users on GitHub from an external identity management system, or IdP.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: GitHub Enterprise Cloud
support_level: community
---
## What is GitHub Enterprise Cloud
## What is GitHub Enterprise Cloud?
> GitHub Enterprise Cloud is a plan for large businesses or teams who collaborate on GitHub.com.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: GitHub Enterprise Server
support_level: community
---
## What is GitHub Enterprise Server
## What is GitHub Enterprise Server?
> GitHub Enterprise Server is a self-hosted platform for software development within your enterprise.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Gitea
support_level: community
---
## What is Gitea
## What is Gitea?
> Gitea is a community managed lightweight code hosting solution written in Go. It is published under the MIT license.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: GitLab
support_level: authentik
---
## What is GitLab
## What is GitLab?
> GitLab is a complete DevOps platform with features for version control, CI/CD, issue tracking, and collaboration, facilitating efficient software development and deployment workflows.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Gravitee
support_level: community
---
## What is Gravitee
## What is Gravitee?
> Gravitee.io API Management is a flexible, lightweight and blazing-fast Open Source solution that helps your organization control who, when and how users access your APIs.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Jenkins
support_level: community
---
## What is Jenkins
## What is Jenkins?
> The leading open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Node-RED
support_level: community
---
## What is Node-RED
## What is Node-RED?
> Node-RED is a programming tool for wiring together hardware devices, APIs and online services in new and interesting ways.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: SonarQube
support_level: community
---
## What is SonarQube
## What is SonarQube?
> Self-managed static analysis tool for continuous codebase inspection
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Weblate
support_level: community
---
## What is Weblate
## What is Weblate?
> Weblate is a copylefted libre software web-based continuous localization system, used by over 2500 libre projects and companies in more than 165 countries.
>

View File

@@ -8,7 +8,7 @@ tags:
authentik_preview: true
---
## What is Fleet
## What is Fleet?
> Fleet is an open source device management (MDM) platform for vulnerability reporting, detection engineering, device health monitoring, posture-based access control, managing unused software licenses, and more.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: MeshCentral
support_level: community
---
## What is MeshCentral
## What is MeshCentral?
> MeshCentral is a free, open source, web-based platform for remote device management.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is AppFlowy
## What is AppFlowy?
> AppFlowy is an open-source workspace collaboration platform (similar to Notion) that lets teams create, manage, and collaborate on documents, databases, and projects.
>

View File

@@ -7,7 +7,7 @@ support_level: community
import TabItem from "@theme/TabItem";
import Tabs from "@theme/Tabs";
## What is BookStack
## What is BookStack?
> BookStack is a free and open-source wiki software aimed for a simple, self-hosted, and easy-to-use platform. It uses the ideas of books to organise pages and store information. BookStack is multilingual and available in over thirty languages. For the simplicity, BookStack is considered as suitable for smaller businesses or freelancers.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: DokuWiki
support_level: community
---
## What is DokuWiki
## What is DokuWiki?
> DokuWiki is an open source wiki application licensed under GPLv2 and written in the PHP programming language. It works on plain text files and thus does not need a database. Its syntax is similar to the one used by MediaWiki and it is often recommended as a more lightweight, easier to customize alternative to MediaWiki.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: GLPI
support_level: community
---
## What is GLPI
## What is GLPI?
> GLPI (Gestionnaire Libre de Parc Informatique) is an open-source IT asset management and service desk software. It helps organizations manage hardware, software, tickets, users, and IT services in a centralized environment.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Karakeep
support_level: community
---
## What is Karakeep
## What is Karakeep?
> A self-hostable bookmark-everything app (links, notes and images) with AI-based automatic tagging and full-text search.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: KitchenOwl
support_level: community
---
## What is KitchenOwl
## What is KitchenOwl?
> KitchenOwl is a smart self-hosted grocery list and recipe manager. Easily add items to your shopping list before you go shopping. You can also create recipes and set up meal plans to help you organize your cooking.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Mealie
support_level: community
---
## What is Mealie
## What is Mealie?
> Mealie is a self hosted recipe manager and meal planner. Easily add recipes by providing the url and Mealie will automatically import the relevant data or add a family recipe with the UI editor.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: NetBox
support_level: community
---
## What is NetBox
## What is NetBox?
> NetBox is the leading solution for modeling and documenting modern networks.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Outline
support_level: community
---
## What is Outline
## What is Outline?
> Your team's knowledge base.
> Lost in a mess of Docs? Never quite sure who has access? Colleagues requesting the same information repeatedly in chat? Its time to get your teams knowledge organized.

View File

@@ -4,7 +4,7 @@ sidebar_label: Paperless-ng
support_level: community
---
## What is Paperless-ng
## What is Paperless-ng?
> Paperless-ng is an application that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents. It was a fork from the original Paperless that is no longer maintained.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Paperless-ngx
support_level: community
---
## What is Paperless-ngx
## What is Paperless-ngx?
> Paperless-ngx is an application that indexes your scanned documents and allows you to easily search for documents and store metadata alongside your documents. It was a fork from Paperless-ng, in turn a fork from the original Paperless, neither of which are maintained any longer.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Papra
support_level: community
---
## What is Papra
## What is Papra?
> An open-source document management platform designed to help you organize, secure, and archive your files effortlessly.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Snipe-IT
support_level: community
---
## What is Snipe-IT
## What is Snipe-IT?
> A free open source IT asset/license management system.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Tandoor
support_level: community
---
## What is Tandoor
## What is Tandoor?
> Application for managing recipes, planning meals and building shopping lists.
>

View File

@@ -4,7 +4,7 @@ sidebar_label: Wiki.js
support_level: community
---
## What is Wiki.js
## What is Wiki.js?
> Wiki.js is a wiki engine running on Node.js and written in JavaScript. It is free software released under the Affero GNU General Public License. It is available as a self-hosted solution or using "single-click" install on the DigitalOcean and AWS marketplace.
>

Some files were not shown because too many files have changed in this diff Show More