Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 181 additions & 0 deletions js/apps/account-ui-lit/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
# Keycloak Account UI (Lit/Web Components)

A new version of the Keycloak Account Console built with [Lit](https://lit.dev/) and PatternFly 6 CSS.

## Features

- **Modern Web Components**: Built entirely with Lit and custom elements
- **PatternFly 6**: Uses PatternFly 6 CSS classes for consistent styling
- **No React dependency**: Framework-agnostic implementation
- **No build step required**: Pure JavaScript ES modules - no TypeScript, no bundler
- **Dynamic page loading**: Pages loaded on-demand based on content.json
- **Same functionality**: All features from the React version are available

## Zero Build Architecture

This theme requires **no Node.js or build tools** to use. The source files are plain JavaScript ES modules that run directly in the browser via import maps.

```
┌─────────────────────────────────────────────────────────────┐
│ Browser │
├─────────────────────────────────────────────────────────────┤
│ Import Map (resolves bare specifiers to vendor files) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ lit → /vendor/lit/lit.js ││
│ │ keycloak-js → /vendor/keycloak-js/keycloak.js ││
│ │ i18next → /vendor/i18next/i18next.js ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ main.js → components/kc-app.js │
│ ↓ │
│ content.json (defines routes & page components) │
│ ↓ │
│ Dynamic import: pages/personal-info.js, pages/groups.js... │
└─────────────────────────────────────────────────────────────┘
```

## Pages

- **Personal Info**: View and edit user profile information
- **Account Security**:
- Signing In: Manage authentication credentials
- Device Activity: View and manage active sessions
- Linked Accounts: Link/unlink external identity providers
- **Applications**: Manage application consents
- **Groups**: View group memberships
- **Organizations**: View organization memberships
- **Resources**: Manage UMA resources (when enabled)

## Development

### Prerequisites (for development only)

- Node.js 18+ (only needed for Vite dev server)

### Development Server

```bash
npm install
npm run dev
```

This starts the Vite development server at `http://localhost:5174`.

### Build (for distribution)

```bash
npm run build
```

This simply copies the JavaScript source files to `target/classes/theme/keycloak.v3/account/resources`. No compilation or bundling is performed.

## Project Structure

```
src/
├── api/ # API methods and type definitions
│ ├── fetch-content.js # Loads content.json
│ ├── methods.js # API request functions
│ ├── representations.js # JSDoc type definitions
│ └── request.js # HTTP request utility
├── components/ # Reusable web components
│ ├── kc-app.js # Main app shell (masthead, sidebar, routing)
│ ├── kc-nav.js # Navigation sidebar (driven by content.json)
│ └── ui/ # Reusable UI component helpers
│ ├── kc-button.js
│ ├── kc-input.js
│ ├── kc-select.js
│ ├── kc-dropdown.js
│ ├── kc-spinner.js
│ ├── kc-alert.js
│ ├── kc-empty-state.js
│ └── index.js # Re-exports all UI components
├── pages/ # Page components (loaded dynamically)
│ ├── personal-info.js
│ ├── signing-in.js
│ ├── device-activity.js
│ ├── linked-accounts.js
│ ├── applications.js
│ ├── groups.js
│ ├── organizations.js
│ └── resources.js
├── types/ # Shared type definitions (JSDoc)
│ └── menu.js # MenuItem type
├── utils/ # Utility functions
├── environment.js # Environment configuration
├── i18n.js # Internationalization
├── keycloak-context.js # Lit context for Keycloak instance
└── main.js # Application entry point
public/
└── content.json # Navigation and routing configuration
```

## Adding a New Page

1. Create the page component in `src/pages/my-page.js`:

```javascript
import { LitElement, html } from "lit";
import { ContextConsumer } from "@lit/context";
import { keycloakContext } from "../keycloak-context.js";
import { t } from "../i18n.js";

export class KcMyPage extends LitElement {
createRenderRoot() {
return this;
}

render() {
return html`
<div class="pf-v6-c-content">
<h1>${t("myPageTitle")}</h1>
<p>${t("myPageDescription")}</p>
</div>
<div class="pf-v6-c-card">
<div class="pf-v6-c-card__body">
<p>Page content here</p>
</div>
</div>
`;
}
}

customElements.define("kc-my-page", KcMyPage);
```

2. Add entry to `public/content.json`:

```json
{
"label": "myPage",
"path": "my-page",
"component": "kc-my-page",
"modulePath": "./pages/my-page.js"
}
```

3. Add translation keys to the theme's `messages_en.properties`.

No changes needed to `kc-app.js` or `kc-nav.js`.

## Key Technologies

- **[Lit](https://lit.dev/)**: Fast, lightweight web component library
- **[@patternfly/patternfly](https://www.patternfly.org/)**: PatternFly 6 CSS
- **[@lit/context](https://lit.dev/docs/data/context/)**: Dependency injection for Lit
- **[keycloak-js](https://www.keycloak.org/docs/latest/securing_apps/#_javascript_adapter)**: Keycloak JavaScript adapter
- **[i18next](https://www.i18next.com/)**: Internationalization framework
- **Import Maps**: Browser-native module resolution

## Benefits

- **No build step**: Source files are served directly (just copied during Maven build)
- **No Node.js required**: Theme users don't need to install Node.js
- **Better caching**: Change one file, only that file is invalidated
- **Smaller initial load**: Pages loaded on-demand
- **Easier debugging**: Source matches what runs in browser
- **Framework agnostic**: Web Components work anywhere

## License

Apache License 2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"themes": [
{
"name": "keycloak.v4",
"types": [
"account"
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<!doctype html>
<html lang="${locale}" dir="${localeDir}">
<head>
<meta charset="utf-8">
<link rel="icon" type="${properties.favIconType!'image/svg+xml'}" href="${resourceUrl}${properties.favIcon!'/favicon.svg'}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light${darkMode?then(' dark', '')}">
<meta name="description" content="${properties.description!'The Account Console is a web-based interface for managing your account.'}">
<title>${properties.title!'Account Management'}</title>
<style>
html, body {
margin: 0;
height: 100%;
}

#app {
height: 100%;
}

.container {
padding: 0;
margin: 0;
width: 100%;
}

.keycloak__loading-container {
height: 100vh;
width: 100%;
color: #151515;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
margin: 0;
}

@media (prefers-color-scheme: dark) {
.keycloak__loading-container {
color: #e0e0e0;
background-color: #1b1d21;
}
}

#loading-text {
z-index: 1000;
font-size: 20px;
font-weight: 600;
padding-top: 32px;
}

.pf-v6-c-spinner {
--pf-v6-c-spinner--AnimationDuration: 1.5s;
--pf-v6-c-spinner--diameter: 3.375rem;
--pf-v6-c-spinner--Width: var(--pf-v6-c-spinner--diameter);
--pf-v6-c-spinner--Height: var(--pf-v6-c-spinner--diameter);
width: var(--pf-v6-c-spinner--Width);
height: var(--pf-v6-c-spinner--Height);
animation: pf-v6-c-spinner-animation var(--pf-v6-c-spinner--AnimationDuration) linear infinite;
}

.pf-v6-c-spinner.pf-m-xl {
--pf-v6-c-spinner--diameter: 6rem;
}

.pf-v6-c-spinner__path {
stroke: currentColor;
stroke-width: 8;
stroke-linecap: round;
stroke-dasharray: 283;
stroke-dashoffset: 280;
animation: pf-v6-c-spinner-path var(--pf-v6-c-spinner--AnimationDuration) ease-in-out infinite;
}

@keyframes pf-v6-c-spinner-animation {
to { transform: rotate(360deg); }
}

@keyframes pf-v6-c-spinner-path {
0% { stroke-dashoffset: 280; }
50% { stroke-dashoffset: 70; }
100% { stroke-dashoffset: 280; }
}
</style>
<script type="importmap">
{
"imports": {
"lit": "${resourceCommonUrl}/vendor/lit/lit.js",
"lit/": "${resourceCommonUrl}/vendor/lit/",
"lit/decorators.js": "${resourceCommonUrl}/vendor/lit/decorators.js",
"@lit/context": "${resourceCommonUrl}/vendor/lit-context/context.js",
"keycloak-js": "${resourceCommonUrl}/vendor/keycloak-js/keycloak.js",
"i18next": "${resourceCommonUrl}/vendor/i18next/i18next.js"
}
}
</script>
<link rel="stylesheet" href="${resourceCommonUrl}/vendor/patternfly-v6/patternfly.min.css">
<#if darkMode>
<script type="module" async blocking="render">
const DARK_MODE_CLASS = "${properties.kcDarkModeClass}";
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");

updateDarkMode(mediaQuery.matches);
mediaQuery.addEventListener("change", (event) => updateDarkMode(event.matches));

function updateDarkMode(isEnabled) {
const { classList } = document.documentElement;

if (isEnabled) {
classList.add(DARK_MODE_CLASS);
} else {
classList.remove(DARK_MODE_CLASS);
}
}
</script>
</#if>
<#if !isSecureContext>
<script type="module" src="${resourceCommonUrl}/vendor/web-crypto-shim/web-crypto-shim.js"></script>
</#if>
<#if devServerUrl?has_content>
<script type="module" src="${devServerUrl}/@vite/client"></script>
<script type="module" src="${devServerUrl}/src/main.js"></script>
<#else>
<script type="module" src="${resourceUrl}/main.js"></script>
</#if>
</head>
<body data-page-id="account">
<div id="app">
<kc-app>
<main class="container">
<div class="keycloak__loading-container">
<svg class="pf-v6-c-spinner pf-m-xl" role="progressbar" aria-valuetext="Loading..." viewBox="0 0 100 100" aria-label="Contents">
<circle class="pf-v6-c-spinner__path" cx="50" cy="50" r="45" fill="none"></circle>
</svg>
<div>
<p id="loading-text">Loading the Account Console</p>
</div>
</div>
</main>
</kc-app>
</div>
<noscript>JavaScript is required to use the Account Console.</noscript>
<script>
window.__env__ = {
"serverBaseUrl": "${serverBaseUrl}",
"authUrl": "${authUrl}",
"authServerUrl": "${authServerUrl}",
"realm": "${realm.name}",
"clientId": "${clientId}",
"resourceUrl": "${resourceUrl}",
"logo": "${properties.logo!""}",
"logoUrl": "${properties.logoUrl!""}",
"baseUrl": "${baseUrl}",
"locale": "${locale}",
"referrerName": "${referrerName!""}",
"referrerUrl": "${referrer_uri!""}",
"features": {
"isRegistrationEmailAsUsername": ${realm.registrationEmailAsUsername?c},
"isEditUserNameAllowed": ${realm.editUsernameAllowed?c},
"isInternationalizationEnabled": ${realm.isInternationalizationEnabled()?c},
"isLinkedAccountsEnabled": ${isLinkedAccountsEnabled?c},
"isMyResourcesEnabled": ${(realm.userManagedAccessAllowed && isAuthorizationEnabled)?c},
"isViewOrganizationsEnabled": ${isViewOrganizationsEnabled?c},
"deleteAccountAllowed": ${deleteAccountAllowed?c},
"updateEmailFeatureEnabled": ${updateEmailFeatureEnabled?c},
"updateEmailActionEnabled": ${updateEmailActionEnabled?c},
"isViewGroupsEnabled": ${isViewGroupsEnabled?c},
"isOid4VciEnabled": ${isOid4VciEnabled?c}
},
"scope": "${scope!""}"
};
</script>
</body>
</html>
Loading
Loading