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` `; } 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}
${kpi.deltaKind === 'live' ? html`` : html``}${kpi.delta}
${this.renderSparkline(kpi.spark, kpi.sparkColor)}
`; } 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
ApprovalsOAuth grants
${this.renderApprovalsChart()}
`; } private renderThreatsCard(): TemplateResult { return html`
Needs attention4 openView all →
${this.attention.map((itemArg) => html`
${itemArg.title}
${itemArg.meta}
`)}
`; } private renderApprovalsTable(): TemplateResult { return html`
Recent approvals142 total
AllPendingDenied
${this.approvals.map((approvalArg) => html` `)}
UserActionDeviceStatusWhen
${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 feedlive - #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 regionlast 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`
${labelArg}${requiredArg ? html`*` : html``}
${hintArg ? html`
${hintArg}
` : html``}
${contentArg}
`; } 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}
`)}
`; } private renderOverview(): TemplateResult { return html`
Workspace - Overviewlive

Good morning, Aegir.

Identity activity across @lossless - 142 identities, 9.1k devices online
24h7d30d90d
${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
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``)}
`)} ${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'}
`)}`, html``)} ${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``)}
`)} ${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``)}
${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``}
`)}
`; } 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]}
`)}
`; } 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/
`)}
`)} ${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``)}
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]}
`)}`)}
`; } 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``)}
${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]}
`)}
`; } 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')}
${users.map((userArg) => html``)}
UserEmailOrgsStatusAdminJoined
${userArg[0].split(' ').map((partArg) => partArg[0]).join('')}${userArg[0]}
${userArg[1]}${userArg[2]}${userArg[3]}${userArg[4] ? html`${userArg[4]}` : html`-`}${userArg[5]}
`; } 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')}
${orgs.map((orgArg) => html``)}
OrganisationSlugMembersPlanStatusCreated
${orgArg[0].slice(0,2).toUpperCase()}${orgArg[0]}
${orgArg[1]}${orgArg[2]}${orgArg[3]}${orgArg[4]}${orgArg[5]}
`; } 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.')}
${apps.map((appArg) => html``)}
AppTypeCategoryInstallsStatus
${appArg[0].slice(0,2).toUpperCase()}${appArg[0]}
${appArg[1]}${appArg[2]}${appArg[3]}${appArg[4].replace('_', ' ')}
`; } 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()}
`; } }