import { DeesElement, html, property, state, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-badge.js';
import './idp-icon.js';
declare global {
interface HTMLElementTagNameMap {
'idp-admin-shell': IdpAdminShell;
}
}
type TNavItem = {
id: string;
label: string;
icon: string;
badge?: string;
};
type TKpi = {
label: string;
value: string;
unit?: string;
delta: string;
deltaKind: 'up' | 'live';
sub: string;
accent: string;
spark: number[];
sparkColor: string;
};
type TApproval = {
user: string;
email: string;
hue: string;
action: string;
device: string;
status: 'ok' | 'warn' | 'error' | 'accent';
label: string;
when: string;
};
type TFeedItem = {
dot: 'bl' | 'ok' | 'wn';
title: string;
detail: string;
meta: string;
};
type TAdminPage =
| 'overview'
| 'profile'
| 'security'
| 'sessions'
| 'apps'
| 'org-general'
| 'org-members'
| 'org-apps'
| 'support'
| 'ga-users'
| 'ga-orgs'
| 'ga-apps';
type TOrg = {
id: string;
name: string;
slug: string;
myRole: string;
};
type TConnectedApp = [name: string, scopes: string[], meta: string];
type TOAuthApp = [name: string, clientId: string, grants: string[], description: string];
@customElement('idp-admin-shell')
export class IdpAdminShell extends DeesElement {
public static demo = () => html` `;
public static demoGroups = ['idp.global v3 full pages'];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
color: var(--idp-fg);
}
.shell {
min-height: 900px;
display: grid;
grid-template-columns: 220px minmax(0, 1fr);
overflow: hidden;
border: 1px solid var(--idp-border);
background: var(--idp-bg);
}
.sidebar {
min-height: 900px;
display: flex;
flex-direction: column;
border-right: 1px solid var(--idp-border);
background: var(--idp-bg);
}
.logo-block {
padding: 14px 16px 10px;
border-bottom: 1px solid var(--idp-border);
}
.logo {
display: flex;
align-items: center;
gap: 8px;
}
.logo-icon {
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 7px;
background: var(--idp-primary);
color: var(--idp-primary-fg);
}
.logo-text {
color: var(--idp-fg);
font-size: 13px;
font-weight: 700;
letter-spacing: -0.3px;
}
.nav-wrap {
flex: 1;
padding: 10px 8px;
}
.nav-section {
margin-bottom: 20px;
}
.nav-title {
padding: 0 8px;
margin-bottom: 4px;
color: var(--idp-muted-fg);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.8px;
text-transform: uppercase;
}
.nav-list {
display: grid;
}
.nav-item,
.org-switch,
.plain-button {
border: 0;
font-family: inherit;
}
.nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 7px;
background: transparent;
color: var(--idp-muted-fg);
font-size: 13px;
font-weight: 400;
text-align: left;
}
.nav-item.active {
background: var(--idp-muted);
color: var(--idp-fg);
font-weight: 500;
}
.nav-item idp-icon {
color: currentColor;
}
.nav-item.active idp-icon {
color: var(--idp-accent);
}
.org-switch-wrap {
position: relative;
margin-bottom: 2px;
}
.org-switch {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
border-radius: 7px;
background: transparent;
color: var(--idp-muted-fg);
cursor: pointer;
}
.org-switch:hover {
background: var(--idp-muted);
}
.org-switch.open {
background: var(--idp-muted);
}
.org-switch.open .org-name {
color: var(--idp-accent);
}
.org-switch.open idp-icon {
transform: rotate(180deg);
}
.org-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
z-index: 40;
overflow: hidden;
border: 1px solid var(--idp-border);
border-radius: 10px;
background: var(--idp-card);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
}
.org-menu-title {
padding: 6px 8px 3px;
color: var(--idp-muted-fg);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.7px;
text-transform: uppercase;
}
.org-menu-item,
.org-create {
width: 100%;
display: flex;
align-items: center;
gap: 8px;
border: 0;
background: transparent;
font-family: inherit;
text-align: left;
}
.org-menu-item {
padding: 6px 8px;
cursor: pointer;
}
.org-menu-item.selected {
background: var(--idp-accent-soft);
}
.org-menu-item .org-avatar {
width: 20px;
height: 20px;
font-size: 7px;
}
.org-menu-name {
overflow: hidden;
color: var(--idp-fg);
font-size: 12px;
font-weight: 400;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-menu-item.selected .org-menu-name {
color: var(--idp-accent);
}
.org-menu-role {
color: var(--idp-muted-fg);
font-size: 10px;
}
.org-menu-divider {
height: 1px;
margin: 3px 0;
background: var(--idp-border);
}
.org-create {
padding: 6px 8px 8px;
color: var(--idp-muted-fg);
cursor: pointer;
font-size: 12px;
}
.org-create-icon {
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1.5px dashed var(--idp-border);
border-radius: 5px;
}
.org-name {
flex: 1;
min-width: 0;
overflow: hidden;
color: var(--idp-muted-fg);
font-size: 13px;
font-weight: 400;
text-align: left;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-avatar,
.user-avatar,
.table-avatar,
.region-code {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-family: var(--idp-mono);
font-weight: 700;
}
.org-avatar {
width: 14px;
height: 14px;
border: 1px solid var(--idp-border);
border-radius: 6px;
background: var(--idp-accent-soft);
color: var(--idp-accent);
font-size: 5px;
}
.user-footer {
display: flex;
align-items: center;
gap: 9px;
padding: 10px 12px;
border-top: 1px solid var(--idp-border);
}
.user-avatar {
width: 26px;
height: 26px;
border-radius: 50%;
background: oklch(0.85 0.08 240);
color: oklch(0.3 0.08 240);
font-size: 9px;
}
.user-meta {
flex: 1;
min-width: 0;
}
.user-name,
.user-email {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-name {
color: var(--idp-fg);
font-size: 12px;
font-weight: 500;
}
.user-email {
color: var(--idp-muted-fg);
font-size: 10px;
}
main {
min-width: 0;
overflow: auto;
background: var(--idp-bg);
}
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
padding: 24px 28px 18px;
border-bottom: 1px solid var(--idp-border);
background: var(--idp-bg);
}
.eyebrow-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.eyebrow {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.live-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 1px 7px;
border: 1px solid var(--idp-ok-border);
border-radius: 999px;
background: var(--idp-ok-bg);
color: var(--idp-ok);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 500;
}
.live-dot,
.feed-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--idp-ok);
box-shadow: 0 0 6px var(--idp-ok);
animation: idp-blink 1.6s ease-in-out infinite;
}
@keyframes idp-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
h1,
h2 {
margin: 0;
color: var(--idp-fg);
font-family: var(--idp-display);
letter-spacing: -0.025em;
}
h1 {
font-size: 26px;
font-weight: 700;
line-height: 1.1;
}
.lead {
margin-top: 4px;
color: var(--idp-muted-fg);
font-size: 13px;
}
.lead code,
.mono {
color: var(--idp-info);
font-family: var(--idp-mono);
}
.page-actions {
display: flex;
align-items: center;
gap: 12px;
}
.tabs {
display: inline-flex;
gap: 1px;
padding: 2px;
border: 1px solid var(--idp-border);
border-radius: 6px;
background: var(--idp-bg-2);
}
.tab {
padding: 4px 10px;
border-radius: 4px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
font-weight: 500;
}
.tab.active {
background: var(--idp-muted);
color: var(--idp-fg);
}
.plain-button {
display: inline-flex;
align-items: center;
gap: 6px;
height: 28px;
padding: 5px 10px;
border-radius: 8px;
background: transparent;
color: var(--idp-fg);
font-size: 12px;
font-weight: 500;
white-space: nowrap;
}
.plain-button.outline {
border: 1px solid var(--idp-border);
}
.plain-button.primary {
background: var(--idp-accent);
color: var(--idp-accent-fg);
}
.plain-button.destructive {
background: var(--idp-destructive);
color: #fff;
}
.plain-button.ghost {
color: var(--idp-fg);
}
.plain-button.warn-outline {
border: 1px solid var(--idp-error-border);
color: var(--idp-error);
}
.body {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 28px 32px;
}
.narrow-body {
max-width: 700px;
}
.wide-body {
max-width: 860px;
}
.section-card {
margin-bottom: 16px;
padding: 20px;
border: 1px solid var(--idp-border);
border-radius: var(--idp-radius);
background: var(--idp-card);
}
.section-card.compact {
padding: 14px;
}
.section-headline {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.section-title {
color: var(--idp-fg);
font-size: 14px;
font-weight: 600;
}
.section-description,
.muted {
color: var(--idp-muted-fg);
font-size: 12px;
line-height: 1.5;
}
.form-row {
display: grid;
grid-template-columns: 180px 1fr;
gap: 16px;
align-items: start;
padding: 12px 0;
border-bottom: 1px solid var(--idp-border);
}
.form-row:last-child {
border-bottom: 0;
}
.form-label {
display: flex;
gap: 3px;
color: var(--idp-fg);
font-size: 13px;
font-weight: 500;
}
.form-hint {
margin-top: 2px;
color: var(--idp-muted-fg);
font-size: 12px;
line-height: 1.4;
}
.input,
.select,
.textarea {
box-sizing: border-box;
width: 100%;
border: 1px solid var(--idp-border);
border-radius: 8px;
outline: none;
background: var(--idp-bg);
color: var(--idp-fg);
font-family: inherit;
font-size: 13px;
}
.input,
.select {
height: 34px;
padding: 0 10px;
}
.textarea {
min-height: 96px;
padding: 8px 10px;
resize: vertical;
}
.input-group {
display: flex;
overflow: hidden;
border: 1px solid var(--idp-border);
border-radius: 8px;
background: var(--idp-bg);
}
.input-prefix {
height: 34px;
display: flex;
align-items: center;
padding: 0 8px;
border-right: 1px solid var(--idp-border);
background: var(--idp-muted);
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 12px;
white-space: nowrap;
}
.input-group .input {
border: 0;
border-radius: 0;
background: transparent;
}
.divider {
height: 1px;
margin: 16px 0;
background: var(--idp-border);
}
.code-block {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border: 1px solid var(--idp-border);
border-radius: 7px;
background: var(--idp-muted);
}
.code-block code {
flex: 1;
overflow-wrap: anywhere;
color: var(--idp-fg);
font-family: var(--idp-mono);
font-size: 12px;
}
.danger-zone {
overflow: hidden;
border: 1px solid var(--idp-error-border);
border-radius: var(--idp-radius);
}
.danger-head {
padding: 10px 16px;
border-bottom: 1px solid var(--idp-error-border);
background: var(--idp-error-bg);
color: var(--idp-error);
font-size: 13px;
font-weight: 600;
}
.danger-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 16px;
border-bottom: 1px solid var(--idp-error-border);
background: var(--idp-card);
}
.danger-item:last-child {
border-bottom: 0;
}
.avatar,
.app-avatar,
.icon-tile {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.avatar {
border-radius: 50%;
background: oklch(0.85 0.08 240);
color: oklch(0.3 0.08 240);
font-family: var(--idp-mono);
font-weight: 600;
}
.app-avatar,
.icon-tile {
border: 1px solid var(--idp-border);
border-radius: 10px;
background: var(--idp-muted);
color: var(--idp-muted-fg);
}
.app-avatar {
width: 40px;
height: 40px;
background: var(--idp-accent-soft);
color: var(--idp-accent);
font-size: 14px;
font-weight: 700;
}
.icon-tile {
width: 38px;
height: 38px;
}
.list-stack {
display: grid;
gap: 12px;
}
.row-card {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 16px;
border: 1px solid var(--idp-border);
border-radius: var(--idp-radius);
background: var(--idp-card);
}
.split-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.chip-row {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.switch {
width: 36px;
height: 20px;
position: relative;
flex-shrink: 0;
border-radius: 10px;
background: var(--idp-accent);
}
.switch::after {
content: '';
position: absolute;
top: 2px;
left: 18px;
width: 16px;
height: 16px;
border-radius: 8px;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
.sub-tabs {
width: fit-content;
display: flex;
gap: 2px;
margin-bottom: 16px;
padding: 3px;
border-radius: 8px;
background: var(--idp-muted);
}
.sub-tab {
padding: 5px 12px;
border-radius: 6px;
color: var(--idp-muted-fg);
font-size: 12px;
}
.sub-tab.active {
background: var(--idp-bg);
color: var(--idp-fg);
font-weight: 500;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
}
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.kpi,
.card {
border: 1px solid var(--idp-border);
background: var(--idp-card);
}
.kpi {
position: relative;
min-height: 132px;
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
padding: 18px 20px 14px;
border-radius: 10px;
}
.kpi::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--kpi-accent);
}
.kpi-label {
color: var(--idp-muted-fg);
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.02em;
}
.kpi-value {
color: var(--idp-fg);
font-family: var(--idp-display);
font-size: 30px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: -0.025em;
line-height: 1.1;
}
.kpi-value span {
margin-left: 2px;
color: var(--idp-muted-fg);
font-size: 14px;
font-weight: 500;
}
.kpi-sub {
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 11px;
}
.kpi-foot {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 8px;
margin-top: auto;
}
.delta {
display: inline-flex;
align-items: center;
gap: 4px;
color: var(--idp-ok);
font-family: var(--idp-mono);
font-size: 11px;
font-weight: 600;
}
.sparkline {
width: 84px;
opacity: 0.85;
}
.primary-grid,
.secondary-grid {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(300px, 1fr);
gap: 16px;
}
.card {
overflow: hidden;
border-radius: 8px;
}
.card-head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.chart-card .card-head {
gap: 12px;
padding: 14px 18px;
}
.card-title {
color: var(--idp-fg);
font-size: 13px;
font-weight: 600;
}
.card-subtitle {
margin-top: 2px;
color: var(--idp-muted-fg);
font-size: 11.5px;
}
.legend {
display: flex;
align-items: center;
gap: 14px;
margin-left: auto;
}
.legend span {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--idp-fg-2);
font-size: 11.5px;
}
.legend i {
width: 8px;
height: 2px;
border-radius: 1px;
}
.chart {
padding: 12px 14px 6px;
}
.chart svg {
width: 100%;
height: auto;
display: block;
}
.view-all {
margin-left: auto;
color: var(--idp-muted-fg);
font-size: 11.5px;
}
.attention {
display: grid;
grid-template-columns: 8px 1fr auto;
gap: 12px;
align-items: center;
padding: 13px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.attention-dot,
.feed-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 4px;
background: var(--dot-color);
box-shadow: var(--dot-glow, none);
}
.attention-title {
color: var(--idp-fg);
font-size: 12.5px;
font-weight: 550;
}
.attention-meta {
margin-top: 2px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10.5px;
}
.mini-action {
padding: 4px 8px;
border: 1px solid var(--idp-border);
border-radius: 6px;
background: transparent;
color: var(--idp-fg);
font-family: inherit;
font-size: 11px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border-bottom: 1px solid var(--idp-border-soft);
text-align: left;
}
th {
padding: 9px 16px;
background: var(--idp-muted);
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
td {
padding: 10px 16px;
color: var(--idp-fg-2);
font-size: 12.5px;
}
tbody tr:hover td {
background: var(--idp-bg-2);
}
.row-user {
display: flex;
align-items: center;
gap: 8px;
}
.table-avatar {
width: 22px;
height: 22px;
border: 1px solid var(--idp-border);
border-radius: 50%;
background: var(--idp-muted);
color: var(--avatar-color);
font-size: 9.5px;
font-weight: 600;
}
.row-name {
color: var(--idp-fg);
font-size: 12.5px;
font-weight: 500;
}
.row-email,
.row-mono,
.feed-meta {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11.5px;
}
.side-tabs {
display: flex;
gap: 2px;
margin-left: auto;
}
.side-tab {
padding: 4px 10px;
border-radius: 4px;
color: var(--idp-muted-fg);
font-size: 11px;
}
.side-tab.active {
background: var(--idp-muted);
color: var(--idp-fg);
}
.feed-item {
display: grid;
grid-template-columns: 14px 1fr auto;
gap: 12px;
align-items: center;
padding: 11px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.feed-item:last-child {
border-bottom: 0;
}
.feed-text {
color: var(--idp-fg-2);
font-size: 12.5px;
}
.feed-text strong {
color: var(--idp-fg);
font-weight: 600;
}
.geo-list {
padding: 8px 0;
}
.region {
display: grid;
grid-template-columns: 24px 1fr auto;
gap: 12px;
align-items: center;
padding: 8px 16px;
}
.region-code {
border: 1px solid var(--region-border, var(--idp-border));
border-radius: 3px;
background: var(--region-bg, transparent);
color: var(--region-color, var(--idp-fg-3));
font-size: 10px;
}
.region-title {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
color: var(--idp-fg);
font-size: 12.5px;
}
.bar-track {
height: 4px;
overflow: hidden;
border-radius: 2px;
background: var(--idp-muted);
}
.bar-fill {
width: var(--share);
height: 100%;
background: var(--bar-color, var(--idp-accent));
opacity: 0.85;
}
.region-count {
min-width: 44px;
color: var(--idp-fg);
font-family: var(--idp-mono);
font-size: 11.5px;
text-align: right;
}
@media (max-width: 1120px) {
.shell {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
.kpis {
grid-template-columns: repeat(2, 1fr);
}
.primary-grid,
.secondary-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 720px) {
.page-head,
.body {
padding-left: 20px;
padding-right: 20px;
}
.page-actions,
.legend {
flex-wrap: wrap;
}
.kpis {
grid-template-columns: 1fr;
}
th:nth-child(3),
td:nth-child(3),
th:nth-child(5),
td:nth-child(5) {
display: none;
}
}
`,
];
@property({ type: String })
public accessor page: TAdminPage = 'overview';
@state()
private accessor orgMenuOpen = false;
@state()
private accessor selectedOrg = 'org_foss';
private workspaceNav: TNavItem[] = [
{ id: 'overview', label: 'Overview', icon: 'grid' },
];
private accountNav: TNavItem[] = [
{ id: 'profile', label: 'Profile', icon: 'user' },
{ id: 'security', label: 'Security', icon: 'shield' },
{ id: 'sessions', label: 'Sessions & Devices', icon: 'monitor' },
{ id: 'apps', label: 'Connected Apps', icon: 'grid' },
];
private orgNav: TNavItem[] = [
{ id: 'org-general', label: 'General', icon: 'building' },
{ id: 'org-members', label: 'Members', icon: 'users' },
{ id: 'org-apps', label: 'OAuth Apps', icon: 'box' },
];
private supportNav: TNavItem[] = [
{ id: 'support', label: 'Support', icon: 'shield' },
];
private adminNav: TNavItem[] = [
{ id: 'ga-users', label: 'All Users', icon: 'users' },
{ id: 'ga-orgs', label: 'All Organisations', icon: 'building' },
{ id: 'ga-apps', label: 'Platform Apps', icon: 'globe' },
];
private orgs: TOrg[] = [
{ id: 'org_foss', name: 'Lossless GmbH', slug: 'lossless', myRole: 'owner' },
{ id: 'org_task', name: 'Task VC', slug: 'task', myRole: 'admin' },
{ id: 'org_demo', name: 'Demo Sandbox', slug: 'demo', myRole: 'viewer' },
{ id: 'org_oss', name: 'OpenSource Coop', slug: 'oss-coop', myRole: 'editor' },
{ id: 'org_ext', name: 'External Client', slug: 'ext-client', myRole: 'guest' },
];
private heroKpis: TKpi[] = [
{
label: 'Identities',
value: '2,847',
delta: '↑ 12% wk',
deltaKind: 'up',
spark: [10, 12, 11, 14, 13, 16, 15, 18, 19],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-chart-1)',
sub: '142 added this week',
},
{
label: 'Active devices',
value: '9,140',
delta: '↑ 4.2%',
deltaKind: 'up',
spark: [12, 13, 11, 14, 13, 15, 14, 16, 17],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-chart-2)',
sub: '3.2 avg / identity',
},
{
label: 'Avg approval',
value: '0.8',
unit: 's',
delta: '↓ 60ms faster',
deltaKind: 'up',
spark: [16, 14, 17, 12, 15, 13, 11, 9, 7],
sparkColor: 'var(--idp-spark-info)',
accent: 'var(--idp-chart-5)',
sub: 'p95 - all regions',
},
{
label: 'Cardano anchors',
value: '12,408',
delta: 'synced 4s ago',
deltaKind: 'live',
spark: [8, 9, 11, 10, 13, 12, 15, 16, 18],
sparkColor: 'var(--idp-spark-up)',
accent: 'var(--idp-info)',
sub: 'block #9 841 222',
},
];
private approvals: TApproval[] = [
{ user: 'Jane Doe', email: 'jane@lossless.com', hue: 'var(--idp-chart-1)', action: 'OAuth - GitHub', device: 'iPhone 15 Pro', status: 'ok', label: 'approved', when: '2 min ago' },
{ user: 'Alex Brown', email: 'alex@lossless.com', hue: 'var(--idp-chart-4)', action: 'CLI login', device: 'MacBook Pro', status: 'warn', label: 'pending', when: 'just now' },
{ user: 'Sam Chen', email: 'sam@lossless.com', hue: 'var(--idp-chart-5)', action: 'NFC tap - door 4F', device: 'iPhone 14', status: 'ok', label: 'approved', when: '12 min ago' },
{ user: 'Unknown device', email: 'Lagos - NG', hue: 'var(--idp-chart-3)', action: 'Web login', device: 'Chrome 132', status: 'error', label: 'denied', when: '1 hr ago' },
{ user: 'Maria K.', email: 'maria@lossless.com', hue: 'var(--idp-chart-5)', action: 'Key rotation', device: 'Apple Watch', status: 'accent', label: 'on-chain', when: '3 hr ago' },
];
private attention = [
{ sev: 'error', title: 'Repeated denied logins', meta: '5 attempts - Lagos, NG - last 1h', action: 'Block IP', color: 'var(--idp-error)' },
{ sev: 'warning', title: 'Stale device key', meta: 'sam@ - Apple Watch - 9 mo old', action: 'Rotate', color: 'var(--idp-warn)' },
{ sev: 'warning', title: 'OAuth scope expanding', meta: 'github - repo:write requested', action: 'Review', color: 'var(--idp-warn)' },
{ sev: 'info', title: 'Pending key rotations', meta: '3 identities - Cardano awaiting', action: 'Run', color: 'var(--idp-info)' },
];
private feed: TFeedItem[] = [
{ dot: 'bl', title: 'Identity created', detail: 'did:idp:0x9b12...f034', meta: 'block #9 841 222' },
{ dot: 'ok', title: 'Anchor confirmed', detail: '12 blocks deep', meta: '2m' },
{ dot: 'bl', title: 'Key rotation', detail: 'did:idp:0x4a3f...c819', meta: 'block #9 841 221' },
{ dot: 'ok', title: 'OAuth scope updated', detail: 'github - repo:read', meta: '5m' },
{ dot: 'wn', title: 'Device registered', detail: 'MacBook Pro - pending', meta: '7m' },
{ dot: 'bl', title: 'Anchor submitted', detail: 'awaiting confirmation', meta: '8m' },
];
private get currentOrg(): TOrg {
return this.orgs.find((orgArg) => orgArg.id === this.selectedOrg) || this.orgs[0];
}
private setPage(pageArg: TAdminPage) {
this.page = pageArg;
this.orgMenuOpen = false;
}
private selectOrg(orgIdArg: string) {
this.selectedOrg = orgIdArg;
this.orgMenuOpen = false;
this.page = 'org-general';
}
private renderNavGroup(items: TNavItem[], active = ''): TemplateResult {
return html`
${items.map((item) => html`
this.setPage(item.id as TAdminPage)}>
${item.label}
${item.badge ? html`${item.badge} ` : html``}
`)}
`;
}
private renderSidebar(): TemplateResult {
const currentOrg = this.currentOrg;
return html`
`;
}
private renderSparkline(data: number[], color: string): TemplateResult {
const max = Math.max(...data);
const min = Math.min(...data);
const range = max - min || 1;
const width = 100;
const height = 22;
const points = data.map((valueArg, indexArg) => {
const x = (indexArg / (data.length - 1)) * width;
const y = height - ((valueArg - min) / range) * (height - 4) - 2;
return `${x},${y}`;
}).join(' ');
const area = `0,${height} ${points} ${width},${height}`;
return html`
`;
}
private renderKpi(kpi: TKpi): TemplateResult {
return html`
${kpi.label}
${kpi.value}${kpi.unit ? html`${kpi.unit} ` : html``}
${kpi.sub}
`;
}
private renderApprovalsChart(): TemplateResult {
return html`
${[0, 20, 40, 60, 80].map((tickArg, indexArg) => {
const y = 194 - (tickArg / 80) * 182;
return html`${tickArg} `;
})}
${['00', '04', '08', '12', '16', '20', '23'].map((labelArg, indexArg) => {
const x = 36 + (indexArg / 6) * 672;
return html`${labelArg} `;
})}
`;
}
private renderChartCard(): TemplateResult {
return html`
Approval activity
Hourly - last 24 hours
Approvals OAuth grants
${this.renderApprovalsChart()}
`;
}
private renderThreatsCard(): TemplateResult {
return html`
Needs attention 4 open View all →
${this.attention.map((itemArg) => html`
${itemArg.title}
${itemArg.meta}
${itemArg.action}
`)}
`;
}
private renderApprovalsTable(): TemplateResult {
return html`
Recent approvals 142 total All Pending Denied
User Action Device Status When
${this.approvals.map((approvalArg) => html`
${approvalArg.user.split(' ').map((partArg) => partArg[0]).slice(0, 2).join('').toUpperCase()} ${approvalArg.user}
${approvalArg.email}
${approvalArg.action}
${approvalArg.device}
${approvalArg.label}
${approvalArg.when}
`)}
`;
}
private feedDotStyle(kind: TFeedItem['dot']): string {
if (kind === 'ok') return '--dot-color:var(--idp-ok);--dot-glow:0 0 6px var(--idp-ok-border)';
if (kind === 'wn') return '--dot-color:var(--idp-warn)';
return '--dot-color:var(--idp-info);--dot-glow:0 0 6px var(--idp-info-border)';
}
private renderFeedCard(): TemplateResult {
return html`
Cardano feed live - #9 841 222
${this.feed.map((itemArg) => html`
${itemArg.title} - ${itemArg.detail}
${itemArg.meta}
`)}
`;
}
private renderGeoCard(): TemplateResult {
const regions = [
{ name: 'Berlin, DE', code: 'DE', count: '1,284', share: '100%', kind: 'home' },
{ name: 'Amsterdam, NL', code: 'NL', count: '642', share: '50%' },
{ name: 'San Francisco, US', code: 'US', count: '521', share: '41%' },
{ name: 'London, GB', code: 'GB', count: '384', share: '30%' },
{ name: 'Tokyo, JP', code: 'JP', count: '218', share: '17%' },
{ name: 'Lagos, NG', code: 'NG', count: '12', share: '1%', kind: 'risk' },
];
return html`
Sign-ins by region last 24h
${regions.map((regionArg) => {
const style = regionArg.kind === 'risk'
? '--region-color:var(--idp-error);--region-border:var(--idp-error-border);--region-bg:var(--idp-error-bg);--bar-color:var(--idp-error)'
: regionArg.kind === 'home'
? '--region-color:var(--idp-accent);--region-border:var(--idp-info-border);--region-bg:var(--idp-accent-soft);--bar-color:var(--idp-accent)'
: '';
return html`
${regionArg.code}
${regionArg.name}${regionArg.kind === 'risk' ? html`flagged ` : regionArg.kind === 'home' ? html`primary ` : html``}
${regionArg.count}
`;
})}
`;
}
private renderPageHeader(titleArg: string, descriptionArg: string, actionArg?: TemplateResult): TemplateResult {
return html`
${titleArg} ${descriptionArg}
${actionArg ? html`${actionArg}
` : html``}
`;
}
private renderSectionCard(titleArg: string, descriptionArg: string, contentArg: TemplateResult, actionArg?: TemplateResult): TemplateResult {
return html`
${titleArg || actionArg ? html`
${titleArg}
${descriptionArg ? html`
${descriptionArg}
` : html``}
${actionArg}
` : html``}
${contentArg}
`;
}
private renderFormRow(labelArg: string, hintArg: string, contentArg: TemplateResult, requiredArg = false): TemplateResult {
return html`
`;
}
private renderCodeBlock(valueArg: string): TemplateResult {
return html`${valueArg}
`;
}
private renderDangerZone(itemsArg: Array<{ title: string; description: string; action: string }>): TemplateResult {
return html`
Danger Zone
${itemsArg.map((itemArg) => html`
${itemArg.title}
${itemArg.description}
${itemArg.action}
`)}
`;
}
private renderOverview(): TemplateResult {
return html`
Workspace - Overview live
Good morning, Aegir.
Identity activity across @lossless - 142 identities, 9.1k devices online
24h 7d 30d 90d
Export + New identity
${this.heroKpis.map((kpiArg) => this.renderKpi(kpiArg))}
${this.renderChartCard()}${this.renderThreatsCard()}
${this.renderApprovalsTable()}${this.renderFeedCard()}
${this.renderGeoCard()}
`;
}
private renderProfile(): TemplateResult {
return html`
${this.renderPageHeader('Profile', 'Your personal identity details visible to connected apps.')}
${this.renderSectionCard('Avatar', 'Shown to apps that request your profile.', html`
AM Upload photo JPG, PNG, GIF up to 2 MB
`)}
${this.renderSectionCard('Personal information', '', html`
${this.renderFormRow('Full name', '', html`
`, true)}
${this.renderFormRow('Username', 'Used in your public profile URL', html`
idp.global/user/
`)}
${this.renderFormRow('Email', 'Primary address for login and notifications', html`
`)}
${this.renderFormRow('Mobile number', 'Used for SMS verification', html`
`)}
Save changes
`)}
${this.renderSectionCard('Account status', '', html`
Status
Your account is currently active.
Active
Global admin
You have platform-wide administrative access.
Admin `)}
${this.renderDangerZone([{ title: 'Delete account', description: 'Permanently delete your account and all associated data. This cannot be undone.', action: 'Delete account' }])}
`;
}
private renderSecurity(): TemplateResult {
const passkeys = ['MacBook Pro - Touch ID', 'iPhone 15 Pro - Face ID'];
return html`
${this.renderPageHeader('Security', 'Manage how you authenticate and protect your account.')}
${this.renderSectionCard('Passkeys', 'Biometric or hardware-key authentication - phishing-resistant and passwordless.', html`${passkeys.map((passkeyArg, indexArg) => html`
${passkeyArg}
Added ${indexArg ? '3 days ago' : '10 days ago'} - Last used ${indexArg ? '1h ago' : '5m ago'}
Remove `)}`, html`
Add passkey`)}
${this.renderSectionCard('Password', 'Update your login password. Use a strong, unique password.', html`
${this.renderFormRow('Current password', '', html`
`)}
${this.renderFormRow('New password', 'Minimum 8 characters', html`
`)}
${this.renderFormRow('Confirm password', '', html`
`)}
Update password
`)}
${this.renderSectionCard('Two-factor authentication', '', html`
Authenticator app (TOTP)
Generate one-time codes with an authenticator app.
Enabled
`)}
`;
}
private renderSessions(): TemplateResult {
const sessions = [
['MacBook Pro 16"', 'Chrome 124 - macOS 14.4', '185.220.101.42', 'Current session'],
['iPhone 15 Pro', 'Safari 17 - iOS 17.4', '185.220.101.42', ''],
['iPad Air', 'Safari 17 - iPadOS 17.3', '91.64.18.227', ''],
['Windows PC', 'Firefox 125 - Windows 11', '194.31.186.5', ''],
];
return html`
${this.renderPageHeader('Sessions & Devices', 'Active login sessions across all your devices.', html`Revoke all others `)}
${sessions.map((sessionArg) => html`
${sessionArg[0]} ${sessionArg[3] ? html`${sessionArg[3]} ` : html``}
${sessionArg[1]} - IP: ${sessionArg[2]}
Started 5m ago - Active just now
${sessionArg[3] ? html`` : html`
Revoke `}
`)}
`;
}
private renderAccountApps(): TemplateResult {
const apps: TConnectedApp[] = [
['foss.global', ['Identity', 'Profile', 'Email'], 'Authorized 3 Apr 2026 - Last used 2h ago'],
['task.vc', ['Identity', 'Profile', 'Email', 'Orgs (read)'], 'Authorized 19 Apr 2026 - Last used 1d ago'],
['Acme HR Portal', ['Identity', 'Email'], 'Authorized 4 Mar 2026 - Last used 7d ago'],
];
return html`
${this.renderPageHeader('Connected Apps', 'Third-party apps and services that have OAuth access to your account.')}
${apps.map((appArg) => html`
${appArg[0].slice(0, 2).toUpperCase()} ${appArg[0]}
${(appArg[1] as string[]).map((scopeArg) => html`${scopeArg} `)}
${appArg[2]}
Revoke `)}
`;
}
private renderOrgGeneral(): TemplateResult {
const org = this.currentOrg;
return html`
${this.renderPageHeader('Organisation Settings', 'General configuration for your organisation.')}
${this.renderSectionCard('', '', html`
${org.name.slice(0, 2).toUpperCase()} ${org.name}
idp.global/${org.slug}
${this.renderFormRow('Organisation name', '', html`
`, true)}${this.renderFormRow('URL slug', "Used in your org's public URLs. Changing this may break existing links.", html`
idp.global/org/
`)}
Save changes
`)}
${this.renderSectionCard('Organisation ID', 'Use this identifier when making API calls.', this.renderCodeBlock(org.id))}
${this.renderDangerZone([{ title: 'Transfer ownership', description: 'Transfer this organisation to another user. You will lose admin access.', action: 'Transfer' }, { title: 'Delete organisation', description: 'Permanently deletes this organisation, its members, apps and billing. Cannot be undone.', action: 'Delete org' }])}
`;
}
private renderOrgMembers(): TemplateResult {
const members: Array<[name: string, email: string, role: string, joined: string]> = [
['Alex Mercer', 'alex@lossless.com', 'owner', 'Joined 120d ago'],
['Jordan Kim', 'jordan@lossless.com', 'admin', 'Joined 90d ago'],
['Sam Rivera', 'sam@lossless.com', 'editor', 'Joined 45d ago'],
['Casey Novak', 'casey@lossless.com', 'viewer', 'Joined 10d ago'],
['Riley Chen', 'riley@external.io', 'guest', 'Joined 2d ago'],
];
const invites: Array<[email: string, role: string, meta: string]> = [
['devops@partner.io', 'editor', 'Invited 3d ago - Expires 30 Jul 2026'],
['audit@consulting.de', 'viewer', 'Invited 1d ago - Expires 1 Aug 2026'],
];
return html`
${this.renderPageHeader('Members', '5 members - 2 pending invitations', html` Invite member `)}
Members (5) Pending (2)
${this.renderSectionCard('', '', html`${members.map((memberArg, indexArg) => html`
${memberArg[0].split(' ').map((partArg) => partArg[0]).join('')}
${memberArg[0]}
${memberArg[1]}
${memberArg[3]}
${memberArg[2]}
`)}`)}
${this.renderSectionCard('Pending invitations', '', html`${invites.map((inviteArg, indexArg) => html`
${inviteArg[0]}
${inviteArg[2]}
${inviteArg[1]}
Cancel
`)}`)}
`;
}
private renderOrgApps(): TemplateResult {
const apps: TOAuthApp[] = [
['Internal Dev Portal', 'ci_lossless_devportal_7Xa9', ['auth code'], 'OAuth client for our internal developer tools.'],
['CI Pipeline Auth', 'ci_lossless_ci_4Kp2', ['client credentials'], 'Machine-to-machine auth for deployment pipelines.'],
];
return html`
${this.renderPageHeader('OAuth Apps', "Custom OIDC clients for your organisation's own apps and services.", html` New app `)}
${apps.map((appArg) => html`
${appArg[0].slice(0, 2).toUpperCase()} ${appArg[0]}
${appArg[3]}
${appArg[1]}
${(appArg[2] as string[]).map((grantArg) => html`${grantArg} `)}
`)}
${this.renderSectionCard('OAuth credentials', 'Use these to configure your application.', html`
Client ID
${this.renderCodeBlock('ci_lossless_devportal_7Xa9')}
Redirect URI
${this.renderCodeBlock('https://dev.lossless.com/auth/callback')}
`)}
`;
}
private renderSupport(): TemplateResult {
const services = [
['Account Recovery', 'Lost access to your account or locked out of your organisation? Our team will verify your identity and restore access securely.', 'EUR149', 'per incident', 'key', '1-2 business days'],
['Organisation Recovery', 'All owners have lost access to your organisation. We can verify ownership and restore admin access.', 'EUR249', 'per incident', 'building', '2-3 business days'],
['Data Export & Migration', 'Full export of your org data - users, sessions, app connections - for migration or compliance.', 'EUR199', 'per request', 'box', '3-5 business days'],
['Identity & SSO Consulting', 'Architecture review, OIDC guidance, and custom SSO setup for your organisation stack.', 'EUR190', 'per hour', 'globe', 'Scheduled session'],
['Security Review', 'Audit of connected apps, active sessions, passkey policies, and role assignments.', 'EUR390', 'per review', 'shield', '5-7 business days'],
];
return html`
${this.renderPageHeader('Support', 'idp.global is free for everyone. Paid options cover hands-on recovery and consulting work.')}
idp.global is free, forever
All platform features - authentication, passkeys, OIDC apps, team management - are included at no cost.
${services.map((serviceArg) => html`
${serviceArg[0]} - ${serviceArg[5]}
${serviceArg[1]}
${serviceArg[2]} ${serviceArg[3]}
Request this service `)}
`;
}
private renderGAUsers(): TemplateResult {
const users = [
['Alex Mercer', 'alex@lossless.com', '2', 'active', 'Admin', '200d ago'],
['Jordan Kim', 'jordan@lossless.com', '1', 'active', '', '100d ago'],
['Sam Rivera', 'sam@lossless.com', '1', 'active', '', '45d ago'],
['Dana Walsh', 'dana@suspended.de', '0', 'suspended', '', '60d ago'],
['Morgan Lee', 'morgan@newuser.com', '0', 'new', '', '1d ago'],
];
return html`${this.renderPageHeader('All Users', '6 users on the platform')}`;
}
private renderGAOrgs(): TemplateResult {
const orgs = [['Lossless GmbH', 'lossless', '5', 'Pro', 'active', '200d ago'], ['Task VC', 'task', '3', 'Pro', 'active', '140d ago'], ['Demo Org', 'demo', '1', 'FairUsageFree', 'active', '5d ago'], ['Suspended Co.', 'suspended', '2', 'Pro', 'suspended', '80d ago']];
return html`${this.renderPageHeader('All Organisations', '4 organisations')}Organisation Slug Members Plan Status Created ${orgs.map((orgArg) => html`${orgArg[0].slice(0,2).toUpperCase()} ${orgArg[0]}
${orgArg[1]} ${orgArg[2]} ${orgArg[3]} ${orgArg[4]} ${orgArg[5]} ${orgArg[4] === 'suspended' ? 'Unsuspend' : 'Suspend'} `)}
`;
}
private renderGAApps(): TemplateResult {
const apps = [['foss.global', 'global', 'Productivity', '-', 'active'], ['task.vc', 'global', 'Productivity', '-', 'active'], ['Acme HR', 'partner', 'HR', '412', 'approved'], ['DevOps Suite', 'partner', 'DevOps', '87', 'pending_review']];
return html`${this.renderPageHeader('Platform Apps', 'Global and partner apps across the platform.')}App Type Category Installs Status ${apps.map((appArg) => html`${appArg[0].slice(0,2).toUpperCase()} ${appArg[0]}
${appArg[1]} ${appArg[2]} ${appArg[3]} ${appArg[4].replace('_', ' ')} ${appArg[4] === 'pending_review' ? 'Approve' : 'Suspend'} `)}
`;
}
private renderMainContent(): TemplateResult {
const renderers: Record TemplateResult> = {
overview: () => this.renderOverview(),
profile: () => this.renderProfile(),
security: () => this.renderSecurity(),
sessions: () => this.renderSessions(),
apps: () => this.renderAccountApps(),
'org-general': () => this.renderOrgGeneral(),
'org-members': () => this.renderOrgMembers(),
'org-apps': () => this.renderOrgApps(),
support: () => this.renderSupport(),
'ga-users': () => this.renderGAUsers(),
'ga-orgs': () => this.renderGAOrgs(),
'ga-apps': () => this.renderGAApps(),
};
return (renderers[this.page] || renderers.overview)();
}
public render(): TemplateResult {
return html`
${this.renderSidebar()}
${this.renderMainContent()}
`;
}
}