Files
catalog/ts_web/elements/idp-dashboard-window.ts

594 lines
18 KiB
TypeScript

import { DeesElement, html, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-badge.js';
import './idp-button.js';
import './idp-icon.js';
type TDashboardStat = {
label: string;
value: string;
unit?: string;
delta: string;
sub: string;
accent: string;
sparkColor: string;
spark: number[];
live?: boolean;
};
declare global {
interface HTMLElementTagNameMap {
'idp-dashboard-window': IdpDashboardWindow;
}
}
@customElement('idp-dashboard-window')
export class IdpDashboardWindow extends DeesElement {
public static demo = () => html`<idp-dashboard-window dark></idp-dashboard-window>`;
public static demoGroups = ['idp.global v3 composed surfaces'];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.dash {
position: relative;
overflow: hidden;
border: 1px solid var(--idp-border);
border-radius: 14px;
background: var(--idp-bg-2);
color: var(--idp-fg);
box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 40px 80px -20px rgba(0,0,0,0.70), 0 8px 24px rgba(0,0,0,0.35);
}
.chrome, .appbar, .bottom {
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--idp-border-soft);
background: rgba(255,255,255,0.02);
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
}
.chrome {
padding: 11px 14px;
}
.tdot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.red { background: #ff5f57; }
.yellow { background: #ffbd2e; }
.green { background: #28c840; }
.url, .org, .search {
display: inline-flex;
align-items: center;
gap: 8px;
border: 1px solid var(--idp-border-soft);
border-radius: 5px;
background: var(--idp-bg);
color: var(--idp-fg-3, var(--idp-muted-fg));
}
.url {
margin-left: 12px;
padding: 4px 10px;
}
.status {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 6px;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--idp-ok);
box-shadow: 0 0 8px var(--idp-ok);
animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
.appbar {
justify-content: space-between;
height: 44px;
padding: 0 14px;
background: var(--idp-bg);
}
.appbar-left, .appbar-right {
display: flex;
align-items: center;
gap: 12px;
}
.logo {
display: inline-flex;
align-items: center;
gap: 7px;
color: var(--idp-fg);
font-family: var(--idp-display);
font-size: 13px;
font-weight: 700;
letter-spacing: -0.015em;
}
.logo-dot {
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--idp-accent);
box-shadow: 0 0 12px var(--idp-accent);
}
.divider {
width: 1px;
height: 18px;
background: var(--idp-border);
}
.org {
padding: 4px 10px 4px 4px;
background: var(--idp-bg-2);
font-size: 12px;
}
.avatar-sm {
width: 20px;
height: 20px;
display: inline-grid;
place-items: center;
border-radius: 4px;
background: var(--idp-accent);
color: #fff;
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 700;
}
.search {
min-width: 240px;
height: 28px;
padding: 0 10px;
background: var(--idp-bg-2);
}
.kbd {
margin-left: auto;
padding: 0 5px;
border: 1px solid var(--idp-border);
border-radius: 3px;
background: var(--idp-bg);
font-size: 10px;
}
.user-avatar {
width: 28px;
height: 28px;
display: inline-grid;
place-items: center;
border: 1px solid rgba(59,130,246,0.4);
border-radius: 50%;
background: rgba(0,80,185,0.25);
color: var(--idp-accent-hover);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 700;
}
.shell {
display: grid;
grid-template-columns: 200px 1fr;
min-height: 580px;
}
aside {
display: flex;
flex-direction: column;
gap: 1px;
padding: 12px 8px;
border-right: 1px solid var(--idp-border-soft);
background: var(--idp-bg);
}
.side-label {
padding: 12px 10px 6px;
color: color-mix(in srgb, var(--idp-muted-fg), transparent 35%);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.side-nav {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: 5px;
color: var(--idp-muted-fg);
font-size: 12.5px;
}
.side-nav.active {
background: rgba(0,80,185,0.18);
color: var(--idp-fg);
}
.nav-icon {
width: 18px;
display: inline-flex;
justify-content: center;
color: currentColor;
}
.side-nav.active .nav-icon {
color: var(--idp-accent-hover);
}
main {
min-width: 0;
padding: 22px 24px;
background: var(--idp-bg);
}
.head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
h3 {
margin: 0;
font-family: var(--idp-display);
font-size: 22px;
font-weight: 650;
letter-spacing: -0.02em;
}
.sub {
margin-top: 2px;
color: var(--idp-muted-fg);
font-size: 12.5px;
}
.actions {
display: flex;
gap: 8px;
}
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
margin-bottom: 16px;
}
.stat, .card {
border: 1px solid var(--idp-border-soft);
background: var(--idp-bg-2);
}
.stat {
position: relative;
min-height: 132px;
display: flex;
flex-direction: column;
gap: 4px;
overflow: hidden;
padding: 18px 20px 14px;
border-radius: 10px;
}
.stat::before {
content: '';
position: absolute;
inset: 0 0 auto;
height: 2px;
background: var(--stat-accent);
}
.stat-label {
color: var(--idp-muted-fg);
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.02em;
}
.stat-val {
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;
}
.stat-val span {
margin-left: 2px;
color: var(--idp-muted-fg);
font-size: 14px;
font-weight: 500;
}
.stat-sub {
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 11px;
}
.stat-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;
}
.sparkline svg {
width: 100%;
height: 22px;
display: block;
}
.grid {
display: grid;
grid-template-columns: 1.6fr 1fr;
gap: 16px;
}
.card {
overflow: hidden;
}
.card-head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.card-title {
font-size: 13px;
font-weight: 600;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 10px 16px;
border-bottom: 1px solid var(--idp-border-soft);
text-align: left;
font-size: 12.5px;
}
th {
color: color-mix(in srgb, var(--idp-muted-fg), transparent 35%);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.user {
display: flex;
align-items: center;
gap: 8px;
}
.row-avatar {
width: 22px;
height: 22px;
display: inline-grid;
place-items: center;
border: 1px solid var(--idp-border);
border-radius: 50%;
background: var(--idp-card-2);
color: var(--idp-accent-hover);
font-family: var(--idp-mono);
font-size: 9.5px;
font-weight: 700;
}
.row-name {
color: var(--idp-fg);
font-weight: 500;
}
.row-email, .dim {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
}
.feed-item {
display: grid;
grid-template-columns: 14px 1fr auto;
gap: 12px;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.feed-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--idp-accent-hover);
}
.feed-dot.ok {
background: var(--idp-ok);
}
.feed-text {
color: var(--idp-fg-3, var(--idp-muted-fg));
font-size: 12.5px;
}
.feed-text strong {
color: var(--idp-fg);
font-weight: 500;
}
.feed-meta {
color: color-mix(in srgb, var(--idp-muted-fg), transparent 35%);
font-family: var(--idp-mono);
font-size: 10.5px;
}
.bottom {
height: 28px;
padding: 0 14px;
border-top: 1px solid var(--idp-border-soft);
border-bottom: 0;
background: var(--idp-bg);
}
.bottom .divider {
height: 12px;
}
.grow {
flex: 1;
}
@media (max-width: 900px) {
.shell {
grid-template-columns: 1fr;
}
aside, .search {
display: none;
}
.stats {
grid-template-columns: repeat(2, 1fr);
}
.grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 560px) {
.stats {
grid-template-columns: 1fr;
}
.head {
align-items: flex-start;
flex-direction: column;
}
th:nth-child(3), td:nth-child(3), th:nth-child(4), td:nth-child(4) {
display: none;
}
}
`,
];
private stats: TDashboardStat[] = [
{ label: 'Identities', value: '2,847', delta: '↑ 12% wk', sub: '142 added this week', accent: 'var(--idp-chart-1)', sparkColor: 'var(--idp-spark-up)', spark: [10, 12, 11, 14, 13, 16, 15, 18, 19] },
{ label: 'Active devices', value: '9,140', delta: '↑ 4.2%', sub: '3.2 avg / identity', accent: 'var(--idp-chart-2)', sparkColor: 'var(--idp-spark-up)', spark: [12, 13, 11, 14, 13, 15, 14, 16, 17] },
{ label: 'Avg approval', value: '0.8', unit: 's', delta: '↓ 60ms faster', sub: 'p95 - all regions', accent: 'var(--idp-chart-5)', sparkColor: 'var(--idp-spark-info)', spark: [16, 14, 17, 12, 15, 13, 11, 9, 7] },
{ label: 'Cardano anchors', value: '12,408', delta: 'synced 4s ago', sub: 'block #9 841 222', accent: 'var(--idp-info)', sparkColor: 'var(--idp-spark-up)', spark: [8, 9, 11, 10, 13, 12, 15, 16, 18], live: true },
];
private approvals = [
['Jane Doe', 'jane@lossless.com', 'OAuth - GitHub', 'iPhone 15 Pro', 'approved', 'ok'],
['Alex Brown', 'alex@lossless.com', 'CLI login', 'MacBook Pro', 'pending', 'warn'],
['Sam Chen', 'sam@lossless.com', 'NFC tap - door 4F', 'iPhone 14', 'approved', 'ok'],
['Unknown device', 'Lagos - NG', 'Web login', 'Chrome 132', 'denied', 'error'],
['Maria K.', 'maria@lossless.com', 'Key rotation', 'Apple Watch S9', 'on-chain', 'accent'],
];
private feed = [
['Identity created', 'did:idp:0x9b12...f034', 'block #9 841 222', ''],
['Anchor confirmed', '12 blocks deep', '2m', 'ok'],
['Key rotation', 'did:idp:0x4a3f...c819', 'block #9 841 221', ''],
['OAuth scope updated', 'github repo:read', '5m', 'ok'],
['Device registered', 'MacBook Pro pending', '7m', ''],
];
private workspaceNav = [
['Overview', 'grid'],
['Identities', 'user'],
['Approvals', 'bell'],
['OAuth clients', 'key'],
['Devices', 'monitor'],
['Audit log', 'clock'],
];
private chainNav = [
['Cardano sync', 'wallet'],
['Anchors', 'shield'],
];
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`<svg viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"><polygon points=${area} fill=${color} opacity="0.12"></polygon><polyline points=${points} fill="none" stroke=${color} stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></polyline></svg>`;
}
private renderStat(statArg: TDashboardStat): TemplateResult {
return html`
<div class="stat" style="--stat-accent:${statArg.accent}">
<div class="stat-label">${statArg.label}</div>
<div class="stat-val">${statArg.value}${statArg.unit ? html`<span>${statArg.unit}</span>` : html``}</div>
<div class="stat-sub">${statArg.sub}</div>
<div class="stat-foot"><span class="delta">${statArg.live ? html`<span class="live-dot"></span>` : html``}${statArg.delta}</span><div class="sparkline">${this.renderSparkline(statArg.spark, statArg.sparkColor)}</div></div>
</div>
`;
}
public render(): TemplateResult {
return html`
<div class="dash" theme="dark">
<div class="chrome">
<span class="tdot red"></span><span class="tdot yellow"></span><span class="tdot green"></span>
<span class="url"><idp-icon name="lock" size="11" style="color: var(--idp-ok)"></idp-icon> console.idp.global / dashboard</span>
<span class="status"><span class="live-dot"></span>eu-west-1 - 38ms</span>
</div>
<div class="appbar">
<div class="appbar-left">
<span class="logo">idp<span class="logo-dot"></span>global</span>
<span class="divider"></span>
<span class="org"><span class="avatar-sm">L</span>Lossless GmbH</span>
</div>
<div class="appbar-right">
<span class="search">Search identities, devices <span class="kbd">Cmd+K</span></span>
<span class="user-avatar">AM</span>
</div>
</div>
<div class="shell">
<aside>
<div class="side-label">Workspace</div>
${this.workspaceNav.map((itemArg, indexArg) => html`
<span class="side-nav ${indexArg === 0 ? 'active' : ''}"><span class="nav-icon"><idp-icon name=${itemArg[1] as any} size="13"></idp-icon></span>${itemArg[0]}</span>
`)}
<div class="side-label">On-chain</div>
${this.chainNav.map((itemArg) => html`<span class="side-nav"><span class="nav-icon"><idp-icon name=${itemArg[1] as any} size="13"></idp-icon></span>${itemArg[0]}</span>`)}
</aside>
<main>
<div class="head">
<div>
<h3>Overview</h3>
<div class="sub">Identity activity across <span style="color: var(--idp-accent-hover); font-family: var(--idp-mono)">@lossless</span> - last 7 days</div>
</div>
<div class="actions"><idp-button variant="ghost" size="sm">Export</idp-button><idp-button variant="accent" size="sm">New identity</idp-button></div>
</div>
<div class="stats">
${this.stats.map((statArg) => this.renderStat(statArg))}
</div>
<div class="grid">
<section class="card">
<div class="card-head"><span class="card-title">Recent approvals</span><idp-badge>142 total</idp-badge></div>
<table>
<thead><tr><th>User</th><th>Action</th><th>Device</th><th>Status</th></tr></thead>
<tbody>
${this.approvals.map((rowArg) => html`
<tr>
<td><div class="user"><span class="row-avatar">${rowArg[0].slice(0, 2).toUpperCase()}</span><div><div class="row-name">${rowArg[0]}</div><div class="row-email">${rowArg[1]}</div></div></div></td>
<td>${rowArg[2]}</td>
<td><span class="dim">${rowArg[3]}</span></td>
<td><idp-badge variant=${rowArg[5] as any}>${rowArg[4]}</idp-badge></td>
</tr>
`)}
</tbody>
</table>
</section>
<section class="card">
<div class="card-head"><span class="card-title">Cardano feed</span><idp-badge variant="accent"><span class="live-dot"></span>live</idp-badge></div>
${this.feed.map((itemArg) => html`
<div class="feed-item"><span class="feed-dot ${itemArg[3]}"></span><div class="feed-text"><strong>${itemArg[0]}</strong> - ${itemArg[1]}</div><span class="feed-meta">${itemArg[2]}</span></div>
`)}
</section>
</div>
</main>
</div>
<div class="bottom"><span><span class="live-dot"></span>API - 38ms</span><span class="divider"></span><span>v3.81.0</span><span class="grow"></span><span>block #9 841 222 - confirmed</span></div>
</div>
`;
}
}