feat(catalog): add initial idp.global component catalog with primitives, composed views, and full-page showcases

This commit is contained in:
2026-05-03 10:11:06 +00:00
commit cd5eac437c
35 changed files with 11775 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
/**
* autocreated commitinfo by @push.rocks/commitinfo
*/
export const commitinfo = {
name: '@idp.global/catalog',
version: '1.1.0',
description: 'Web component catalog for idp.global, based on the v3 product design language.'
}
File diff suppressed because it is too large Load Diff
+182
View File
@@ -0,0 +1,182 @@
import { DeesElement, html, property, 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';
declare global {
interface HTMLElementTagNameMap {
'idp-approval-card': IdpApprovalCard;
}
}
@customElement('idp-approval-card')
export class IdpApprovalCard extends DeesElement {
public static demo = () => html`
<idp-approval-card
app-name="GitHub"
app-initials="GH"
app-color="#24292F"
request-text="Sign in to github.com"
location="Berlin · DE"
device="Safari · MacBook Pro"
risk="trusted"
time-label="now"
></idp-approval-card>
`;
public static demoGroups = ['idp.global v3 approval surfaces'];
@property({ type: String, attribute: 'app-name' })
public accessor appName = 'GitHub';
@property({ type: String, attribute: 'app-initials' })
public accessor appInitials = 'GH';
@property({ type: String, attribute: 'app-color' })
public accessor appColor = '#24292F';
@property({ type: String, attribute: 'request-text' })
public accessor requestText = 'Sign in to github.com';
@property({ type: String })
public accessor location = 'Berlin · DE';
@property({ type: String })
public accessor device = 'Safari · MacBook Pro';
@property({ type: String })
public accessor risk: 'trusted' | 'warning' | 'low' = 'trusted';
@property({ type: String, attribute: 'time-label' })
public accessor timeLabel = 'now';
@property({ type: Boolean, reflect: true })
public accessor primary = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.card {
background: var(--idp-card);
border: 1px solid var(--idp-border);
border-radius: 12px;
padding: 14px;
color: var(--idp-fg);
}
:host([primary]) .card {
border-color: var(--idp-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--idp-accent), transparent 92%);
}
.top {
display: flex;
gap: 12px;
align-items: flex-start;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 8px;
background: var(--app-color);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 750;
letter-spacing: -0.03em;
flex: 0 0 auto;
}
.body {
flex: 1;
min-width: 0;
}
.line {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.app {
font-size: 14px;
font-weight: 650;
letter-spacing: -0.02em;
}
.time, .sub, .meta {
color: var(--idp-muted-fg);
}
.time {
font-size: 12px;
}
.sub {
margin-top: 1px;
font-size: 13px;
}
.meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
margin-top: 9px;
font-size: 12px;
}
.meta-item {
display: inline-flex;
align-items: center;
gap: 4px;
}
.actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
idp-button:first-child {
flex: 1;
}
idp-button:last-child {
flex: 2;
}
`,
];
private dispatchAction(actionArg: 'approve' | 'deny') {
this.dispatchEvent(new CustomEvent(`idp-${actionArg}`, {
detail: {
appName: this.appName,
requestText: this.requestText,
},
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
const badgeVariant = this.risk === 'warning' ? 'warn' : 'ok';
const badgeText = this.risk === 'warning' ? 'new network' : 'trusted';
return html`
<article class="card" style="--app-color: ${this.appColor}">
<div class="top">
<div class="avatar">${this.appInitials}</div>
<div class="body">
<div class="line">
<div class="app">${this.appName}</div>
<div class="time">${this.timeLabel}</div>
</div>
<div class="sub">${this.requestText}</div>
<div class="meta">
<span class="meta-item"><idp-icon name="location" size="12"></idp-icon>${this.location}</span>
<span class="meta-item"><idp-icon name="laptop" size="12"></idp-icon>${this.device}</span>
<idp-badge variant=${badgeVariant as any}>${badgeText}</idp-badge>
</div>
</div>
</div>
<div class="actions">
<idp-button variant="outline" @click=${() => this.dispatchAction('deny')}>Deny</idp-button>
<idp-button variant="accent" icon="check" @click=${() => this.dispatchAction('approve')}>Approve</idp-button>
</div>
</article>
`;
}
}
+84
View File
@@ -0,0 +1,84 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
export type TIdpBadgeVariant = 'default' | 'accent' | 'ok' | 'warn' | 'error' | 'outline';
declare global {
interface HTMLElementTagNameMap {
'idp-badge': IdpBadge;
}
}
@customElement('idp-badge')
export class IdpBadge extends DeesElement {
public static demo = () => html`
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
<idp-badge>Default</idp-badge>
<idp-badge variant="accent">Admin</idp-badge>
<idp-badge variant="ok">Trusted</idp-badge>
<idp-badge variant="warn">New network</idp-badge>
<idp-badge variant="error">Denied</idp-badge>
</div>
`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor variant: TIdpBadgeVariant = 'default';
public static styles = [
...idpElementStyles,
css`
:host {
display: inline-flex;
}
.badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid transparent;
border-radius: 999px;
font-family: var(--idp-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 16px;
}
.default {
background: var(--idp-muted);
color: var(--idp-muted-fg);
}
.accent {
background: var(--idp-accent-soft);
color: var(--idp-accent);
}
.ok {
background: var(--idp-ok-bg);
color: var(--idp-ok);
border-color: var(--idp-ok-border);
}
.warn {
background: var(--idp-warn-bg);
color: var(--idp-warn);
border-color: var(--idp-warn-border);
}
.error {
background: var(--idp-error-bg);
color: var(--idp-error);
border-color: var(--idp-error-border);
}
.accent {
border-color: var(--idp-info-border);
}
.outline {
background: transparent;
color: var(--idp-fg);
border-color: var(--idp-border);
}
`,
];
public render(): TemplateResult {
return html`<span class="badge ${this.variant}"><slot></slot></span>`;
}
}
+145
View File
@@ -0,0 +1,145 @@
import {
DeesElement,
html,
property,
customElement,
css,
type TemplateResult,
} from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-icon.js';
export type TIdpButtonVariant = 'default' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'accent';
export type TIdpButtonSize = 'sm' | 'md' | 'lg';
declare global {
interface HTMLElementTagNameMap {
'idp-button': IdpButton;
}
}
@customElement('idp-button')
export class IdpButton extends DeesElement {
public static demo = () => html`
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<idp-button>Default</idp-button>
<idp-button variant="accent">Approve</idp-button>
<idp-button variant="outline">Deny</idp-button>
<idp-button variant="ghost">Ghost</idp-button>
<idp-button variant="destructive">Delete</idp-button>
</div>
`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor variant: TIdpButtonVariant = 'default';
@property({ type: String })
public accessor size: TIdpButtonSize = 'md';
@property({ type: String })
public accessor icon = '';
@property({ type: Boolean, reflect: true })
public accessor disabled = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: inline-block;
}
:host([disabled]) {
pointer-events: none;
}
button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
border: 1px solid transparent;
border-radius: 6px;
font-family: var(--idp-font);
font-weight: 500;
letter-spacing: -0.01em;
white-space: nowrap;
cursor: pointer;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease, transform 80ms ease;
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:focus-visible {
outline: 2px solid color-mix(in srgb, var(--idp-accent), transparent 68%);
outline-offset: 2px;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.sm {
height: 32px;
padding: 0 12px;
font-size: 13px;
}
.md {
height: 38px;
padding: 0 14px;
font-size: 14px;
}
.lg {
height: 44px;
padding: 0 18px;
font-size: 15px;
}
.default {
background: var(--idp-primary);
color: var(--idp-primary-fg);
}
.default:hover:not(:disabled) {
opacity: 0.88;
}
.accent {
background: var(--idp-accent);
color: #fff;
box-shadow: 0 4px 14px color-mix(in srgb, var(--idp-accent), transparent 64%);
}
.accent:hover:not(:disabled) {
background: var(--idp-accent-hover);
}
.secondary {
background: var(--idp-muted);
color: var(--idp-fg);
border-color: var(--idp-border);
}
.outline {
background: transparent;
color: var(--idp-fg);
border-color: var(--idp-border);
}
.outline:hover:not(:disabled), .secondary:hover:not(:disabled), .ghost:hover:not(:disabled) {
background: var(--idp-muted);
}
.ghost {
background: transparent;
color: var(--idp-fg);
}
.destructive {
background: var(--idp-destructive);
color: #fff;
}
idp-icon {
flex: 0 0 auto;
}
`,
];
public render(): TemplateResult {
return html`
<button class="${this.variant} ${this.size}" ?disabled=${this.disabled} part="button">
${this.icon ? html`<idp-icon name=${this.icon as any} size="14"></idp-icon>` : html``}
<slot></slot>
</button>
`;
}
}
+76
View File
@@ -0,0 +1,76 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
declare global {
interface HTMLElementTagNameMap {
'idp-card': IdpCard;
}
}
@customElement('idp-card')
export class IdpCard extends DeesElement {
public static demo = () => html`<idp-card headline="Card title" description="Muted supporting text.">Card content</idp-card>`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor headline = '';
@property({ type: String })
public accessor description = '';
@property({ type: Boolean, reflect: true })
public accessor elevated = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.card {
background: var(--idp-card);
border: 1px solid var(--idp-border);
border-radius: var(--idp-radius-lg);
color: var(--idp-fg);
padding: 20px;
}
:host([elevated]) .card {
box-shadow: 0 8px 24px -10px rgb(0 0 0 / 0.28);
}
.head {
display: flex;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
}
.headline {
font-size: 14px;
font-weight: 650;
line-height: 1.35;
}
.description {
color: var(--idp-muted-fg);
font-size: 13px;
line-height: 1.5;
margin-top: 3px;
}
`,
];
public render(): TemplateResult {
return html`
<section class="card" part="card">
${this.headline || this.description ? html`
<div class="head">
<div>
${this.headline ? html`<div class="headline">${this.headline}</div>` : html``}
${this.description ? html`<div class="description">${this.description}</div>` : html``}
</div>
<slot name="action"></slot>
</div>
` : html``}
<slot></slot>
</section>
`;
}
}
+593
View File
@@ -0,0 +1,593 @@
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>
`;
}
}
+263
View File
@@ -0,0 +1,263 @@
import {
DeesElement,
html,
property,
customElement,
css,
type TemplateResult,
} from '@design.estate/dees-element';
import * as lucideIcons from 'lucide';
import {
Activity,
ArrowUp,
Bell,
Bolt,
Box,
Building2,
Check,
ChevronDown,
ChevronRight,
Clock,
Cloud,
Copy,
CreditCard,
Fingerprint,
Globe,
Grid2x2,
Home,
Key,
Laptop,
Lock,
LogOut,
Mail,
MapPin,
Monitor,
MonitorSmartphone,
Nfc,
Phone,
Plus,
Power,
QrCode,
Search,
Settings,
Shield,
SmartphoneNfc,
SquarePen,
Trash2,
TriangleAlert,
User,
Users,
Wallet,
X,
createElement,
type IconNode,
} from 'lucide';
import { idpElementStyles } from './tokens.js';
export type TIdpIconName =
| 'activity'
| 'alert'
| 'alert-triangle'
| 'arrow-up'
| 'bell'
| 'bolt'
| 'box'
| 'building'
| 'building2'
| 'building-2'
| 'check'
| 'chevron'
| 'chevron-down'
| 'chevron-right'
| 'clock'
| 'cloud'
| 'copy'
| 'credit'
| 'device'
| 'edit'
| 'fingerprint'
| 'gear'
| 'globe'
| 'grid'
| 'home'
| 'key'
| 'laptop'
| 'location'
| 'lock'
| 'logout'
| 'mail'
| 'monitor'
| 'monitor-smartphone'
| 'nfc'
| 'phone'
| 'plus'
| 'power'
| 'qr'
| 'search'
| 'settings'
| 'shield'
| 'smartphone-nfc'
| 'trash'
| 'user'
| 'users'
| 'wallet'
| 'waveform'
| 'x'
| `lucide:${string}`;
declare global {
interface HTMLElementTagNameMap {
'idp-icon': IdpIcon;
}
}
const iconNodes: Record<string, IconNode> = {
activity: Activity,
alert: TriangleAlert,
'alert-triangle': TriangleAlert,
'arrow-up': ArrowUp,
bell: Bell,
bolt: Bolt,
box: Box,
building: Building2,
building2: Building2,
'building-2': Building2,
check: Check,
chevron: ChevronRight,
'chevron-down': ChevronDown,
'chevron-right': ChevronRight,
clock: Clock,
cloud: Cloud,
copy: Copy,
credit: CreditCard,
device: MonitorSmartphone,
edit: SquarePen,
fingerprint: Fingerprint,
gear: Settings,
globe: Globe,
grid: Grid2x2,
home: Home,
key: Key,
laptop: Laptop,
location: MapPin,
lock: Lock,
logout: LogOut,
mail: Mail,
monitor: Monitor,
'monitor-smartphone': MonitorSmartphone,
nfc: Nfc,
phone: Phone,
plus: Plus,
power: Power,
qr: QrCode,
search: Search,
settings: Settings,
shield: Shield,
'smartphone-nfc': SmartphoneNfc,
trash: Trash2,
user: User,
users: Users,
wallet: Wallet,
waveform: Activity,
x: X,
};
const toKebab = (valueArg: string): string => valueArg
.replace(/^lucide:/, '')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.toLowerCase();
const toLucideExportName = (valueArg: string): string => valueArg
.replace(/^lucide:/i, '')
.split(/[-_: ]+/)
.filter(Boolean)
.map((partArg) => `${partArg.charAt(0).toUpperCase()}${partArg.slice(1)}`)
.join('');
@customElement('idp-icon')
export class IdpIcon extends DeesElement {
public static demo = () => html`<idp-icon name="shield"></idp-icon>`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor name: TIdpIconName = 'shield';
@property({ type: Number })
public accessor size = 18;
private lastRenderKey = '';
public static styles = [
...idpElementStyles,
css`
:host {
display: inline-flex;
align-items: center;
justify-content: center;
color: currentColor;
line-height: 0;
vertical-align: middle;
}
#iconContainer {
width: var(--icon-size);
height: var(--icon-size);
}
#iconContainer svg {
display: block;
width: 100%;
height: 100%;
fill: none;
stroke: currentColor;
stroke-width: 1.75;
stroke-linecap: round;
stroke-linejoin: round;
}
`,
];
private resolveIconNode(): IconNode {
const rawName = String(this.name || 'shield');
const iconName = toKebab(rawName);
const aliasNode = iconNodes[iconName];
if (aliasNode) {
return aliasNode;
}
const exportName = toLucideExportName(rawName);
const lucideNode = (lucideIcons as Record<string, unknown>)[exportName];
if (Array.isArray(lucideNode)) {
return lucideNode as IconNode;
}
return Shield;
}
public render(): TemplateResult {
return html`
<div id="iconContainer" style="--icon-size: ${this.size}px"></div>
`;
}
public updated(): void {
const renderKey = `${this.name}:${this.size}`;
if (this.lastRenderKey === renderKey) {
return;
}
this.lastRenderKey = renderKey;
const container = this.shadowRoot?.querySelector('#iconContainer');
if (!container) {
return;
}
container.innerHTML = '';
const iconElement = createElement(this.resolveIconNode(), {
color: 'currentColor',
size: this.size,
strokeWidth: 1.75,
});
iconElement.setAttribute('aria-hidden', 'true');
container.appendChild(iconElement);
}
}
+192
View File
@@ -0,0 +1,192 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-approval-card.js';
import './idp-badge.js';
import './idp-button.js';
import './idp-icon.js';
import './idp-mobile-frame.js';
declare global {
interface HTMLElementTagNameMap {
'idp-inbox-preview': IdpInboxPreview;
}
}
@customElement('idp-inbox-preview')
export class IdpInboxPreview extends DeesElement {
public static demo = () => html`<idp-inbox-preview></idp-inbox-preview>`;
public static demoGroups = ['idp.global v3 approval surfaces'];
@property({ type: Boolean, reflect: true })
public accessor dark = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
idp-mobile-frame {
--idp-bg: #ffffff;
}
:host([dark]) idp-mobile-frame {
--idp-bg: #09090b;
--idp-fg: #fafafa;
}
.screen {
height: 100%;
background: var(--idp-bg);
color: var(--idp-fg);
box-sizing: border-box;
padding-top: 58px;
display: flex;
flex-direction: column;
}
.header {
padding: 0 20px 14px;
border-bottom: 1px solid var(--idp-border);
}
.brandline {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 650;
}
.brandmark {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--idp-primary);
color: var(--idp-primary-fg);
}
h2 {
margin: 0;
font-size: 24px;
font-weight: 760;
letter-spacing: -0.04em;
}
.summary {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
color: var(--idp-muted-fg);
font-size: 13px;
}
.list {
display: grid;
gap: 10px;
padding: 14px 16px 120px;
overflow: auto;
}
.earlier {
margin: 10px 0 2px;
color: var(--idp-muted-fg);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.row {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
border: 1px solid var(--idp-border);
border-radius: 10px;
font-size: 13px;
}
.row-icon {
width: 26px;
height: 26px;
border-radius: 7px;
background: var(--idp-muted);
color: var(--idp-ok);
display: flex;
align-items: center;
justify-content: center;
}
.row-sub {
color: var(--idp-muted-fg);
font-size: 12px;
margin-top: 1px;
}
.tabbar {
position: absolute;
left: 12px;
right: 12px;
bottom: 24px;
z-index: 80;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
padding: 8px;
border: 1px solid color-mix(in srgb, var(--idp-border), transparent 20%);
border-radius: 22px;
background: color-mix(in srgb, var(--idp-card), transparent 8%);
box-shadow: 0 16px 36px rgb(0 0 0 / 0.16);
backdrop-filter: blur(24px) saturate(170%);
}
.tab {
display: grid;
place-items: center;
gap: 3px;
min-height: 44px;
color: var(--idp-muted-fg);
font-size: 10px;
font-weight: 600;
}
.tab.active {
color: var(--idp-accent);
}
`,
];
public render(): TemplateResult {
return html`
<idp-mobile-frame ?dark=${this.dark}>
<div class="screen">
<header class="header">
<div class="brandline">
<div class="brand"><span class="brandmark"><idp-icon name="shield" size="14"></idp-icon></span>idp.global</div>
<idp-button variant="ghost" size="sm" icon="search" aria-label="Search"></idp-button>
</div>
<h2>Inbox</h2>
<div class="summary"><idp-badge variant="ok">3 pending</idp-badge><span>oldest 8 min ago</span></div>
</header>
<div class="list">
<idp-approval-card primary app-name="GitHub" app-initials="GH" app-color="#24292F" request-text="Sign in to github.com" location="Berlin · DE" device="Safari · MBP" time-label="now"></idp-approval-card>
<idp-approval-card app-name="Lufthansa.com" app-initials="LH" app-color="#05164D" request-text="Verify identity" location="Berlin · DE" device="iPhone 15 Pro" time-label="2m"></idp-approval-card>
<idp-approval-card app-name="Hetzner Cloud" app-initials="HZ" app-color="#D50C2D" request-text="Sign in to console" location="Falkenstein · DE" device="Firefox · Windows" risk="warning" time-label="8m"></idp-approval-card>
<div class="earlier">Earlier today</div>
${['Notion · Approved 11:42', 'Apple ID · Approved 09:18', 'reddit.com · Denied 08:57'].map((itemArg) => html`
<div class="row">
<div class="row-icon"><idp-icon name="check" size="15"></idp-icon></div>
<div>
<div>${itemArg.split(' · ')[0]}</div>
<div class="row-sub">${itemArg.split(' · ')[1]}</div>
</div>
</div>
`)}
</div>
<nav class="tabbar">
<div class="tab active"><idp-icon name="bell" size="18"></idp-icon>Inbox</div>
<div class="tab"><idp-icon name="clock" size="18"></idp-icon>History</div>
<div class="tab"><idp-icon name="device" size="18"></idp-icon>Devices</div>
<div class="tab"><idp-icon name="user" size="18"></idp-icon>Identity</div>
</nav>
</div>
</idp-mobile-frame>
`;
}
}
+101
View File
@@ -0,0 +1,101 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
declare global {
interface HTMLElementTagNameMap {
'idp-input': IdpInput;
}
}
@customElement('idp-input')
export class IdpInput extends DeesElement {
public static demo = () => html`<idp-input label="Email" placeholder="user@example.com"></idp-input>`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor label = '';
@property({ type: String })
public accessor hint = '';
@property({ type: String })
public accessor value = '';
@property({ type: String })
public accessor placeholder = '';
@property({ type: String })
public accessor type = 'text';
@property({ type: Boolean, reflect: true })
public accessor disabled = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
label {
display: grid;
gap: 6px;
}
.label {
color: var(--idp-fg);
font-size: 13px;
font-weight: 500;
}
input {
width: 100%;
height: 36px;
box-sizing: border-box;
padding: 0 10px;
border: 1px solid var(--idp-border);
border-radius: 8px;
outline: none;
background: var(--idp-card);
color: var(--idp-fg);
font-family: var(--idp-font);
font-size: 13px;
transition: border-color 120ms ease, box-shadow 120ms ease;
}
input:focus {
border-color: var(--idp-accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--idp-accent), transparent 86%);
}
input:disabled {
opacity: 0.5;
}
.hint {
color: var(--idp-muted-fg);
font-size: 12px;
line-height: 1.4;
}
`,
];
private handleInput(eventArg: Event) {
this.value = (eventArg.target as HTMLInputElement).value;
this.dispatchEvent(new CustomEvent('idp-input-change', {
detail: { value: this.value },
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<label>
${this.label ? html`<span class="label">${this.label}</span>` : html``}
<input
.value=${this.value}
type=${this.type}
placeholder=${this.placeholder}
?disabled=${this.disabled}
@input=${this.handleInput}
/>
${this.hint ? html`<span class="hint">${this.hint}</span>` : html``}
</label>
`;
}
}
+165
View File
@@ -0,0 +1,165 @@
import { DeesElement, html, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-button.js';
import './idp-dashboard-window.js';
import './idp-icon.js';
declare global {
interface HTMLElementTagNameMap {
'idp-landing-hero': IdpLandingHero;
}
}
@customElement('idp-landing-hero')
export class IdpLandingHero extends DeesElement {
public static demo = () => html`<idp-landing-hero></idp-landing-hero>`;
public static demoGroups = ['idp.global v3 composed surfaces'];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.hero {
position: relative;
overflow: hidden;
background: #0a0a0a;
color: #fafafa;
border-bottom: 1px solid #1c1c1c;
}
.grid {
position: absolute;
inset: 0;
background-image: linear-gradient(to right, rgba(255,255,255,0.025) 1px, transparent 1px), linear-gradient(to bottom, rgba(255,255,255,0.025) 1px, transparent 1px);
background-size: 56px 56px;
mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 30%, transparent 70%);
pointer-events: none;
}
.glow {
position: absolute;
top: -220px;
left: 50%;
width: 900px;
height: 600px;
transform: translateX(-50%);
background: radial-gradient(ellipse, rgba(59,130,246,0.18) 0%, transparent 60%);
pointer-events: none;
}
.inner {
position: relative;
z-index: 1;
max-width: 1240px;
margin: 0 auto;
padding: 96px 32px 0;
text-align: center;
}
.badge {
display: inline-flex;
align-items: center;
gap: 7px;
margin-bottom: 28px;
padding: 5px 12px 5px 8px;
border: 1px solid #262626;
border-radius: 999px;
background: rgba(255,255,255,0.04);
color: hsl(0 0% 70%);
font-size: 12px;
}
.pill {
padding: 2px 7px;
border-radius: 999px;
background: rgba(59,130,246,0.18);
color: #60a5fa;
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
}
h1 {
margin: 0 0 24px;
font-size: clamp(44px, 6.5vw, 78px);
font-family: var(--idp-display);
font-weight: 700;
letter-spacing: -0.035em;
line-height: 0.96;
}
h1 em {
color: #60a5fa;
font-family: var(--idp-serif);
font-style: italic;
font-weight: 400;
}
.sub {
max-width: 660px;
margin: 0 auto 36px;
color: hsl(0 0% 70%);
font-size: clamp(16px, 1.6vw, 19px);
line-height: 1.55;
}
.actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
align-items: center;
justify-content: center;
margin-bottom: 20px;
}
.fineprint {
color: hsl(0 0% 28%);
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.04em;
}
.fineprint span + span::before {
content: '*';
margin: 0 10px;
}
.product {
position: relative;
max-width: 1180px;
margin: 72px auto 0;
padding: 0 32px;
}
.product-glow {
position: absolute;
inset: 30% 10% -10%;
background: radial-gradient(ellipse, rgba(59,130,246,0.25) 0%, transparent 60%);
filter: blur(40px);
pointer-events: none;
}
idp-dashboard-window {
position: relative;
z-index: 1;
}
@media (max-width: 760px) {
.inner {
padding: 72px 20px 0;
}
}
`,
];
public render(): TemplateResult {
return html`
<section class="hero">
<div class="grid"></div>
<div class="glow"></div>
<div class="inner">
<div class="badge"><span class="pill">v3.81</span>Cardano-anchored identity, now self-hostable</div>
<h1>One identity.<br/><em>Any scale.</em> Yours forever.</h1>
<p class="sub">An open identity provider for everyone, from a single person to a global enterprise. Anchored to the Cardano blockchain so it can never be erased, taken away, or quietly deprecated.</p>
<div class="actions">
<idp-button variant="accent" size="lg" icon="shield">Claim your identity - free</idp-button>
<idp-button variant="ghost" size="lg" icon="globe">View source</idp-button>
</div>
<div class="fineprint"><span>MIT licensed</span><span>Self-hostable</span><span>No credit card</span><span>Cardano mainnet</span></div>
<div class="product">
<div class="product-glow"></div>
<idp-dashboard-window dark></idp-dashboard-window>
</div>
</div>
</section>
`;
}
}
+668
View File
@@ -0,0 +1,668 @@
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';
import './idp-landing-hero.js';
declare global {
interface HTMLElementTagNameMap {
'idp-landing-page': IdpLandingPage;
}
}
@customElement('idp-landing-page')
export class IdpLandingPage extends DeesElement {
public static demo = () => html`<idp-landing-page></idp-landing-page>`;
public static demoGroups = ['idp.global v3 full pages'];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
--idp-bg: #0a0a0a;
--idp-bg-2: #111111;
--idp-card: #121212;
--idp-card-2: #161616;
--idp-fg: #fafafa;
--idp-fg-2: #d4d4d8;
--idp-fg-3: hsl(0 0% 70%);
--idp-muted-fg: hsl(0 0% 55%);
--idp-border: #262626;
--idp-border-soft: #1c1c1c;
--idp-border-strong: #333333;
--idp-accent: #3b82f6;
--idp-accent-hover: #60a5fa;
background: var(--idp-bg);
color: var(--idp-fg);
}
.page {
min-height: 100vh;
background: var(--idp-bg);
}
nav {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
height: 56px;
max-width: 1240px;
margin: 0 auto;
padding: 0 32px;
border-bottom: 1px solid var(--idp-border-soft);
background: rgba(10,10,10,0.86);
backdrop-filter: blur(14px) saturate(140%);
}
.nav-shell {
position: sticky;
top: 0;
z-index: 20;
border-bottom: 1px solid var(--idp-border-soft);
background: rgba(10,10,10,0.86);
}
.logo {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--idp-display);
font-size: 16px;
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);
}
.links, .actions {
display: flex;
align-items: center;
gap: 6px;
}
.links a {
padding: 6px 12px;
border-radius: 5px;
color: var(--idp-fg-3);
font-size: 13px;
text-decoration: none;
}
.links a:hover {
background: rgba(255,255,255,0.04);
color: var(--idp-fg);
}
.status {
display: inline-flex;
align-items: center;
gap: 6px;
margin-right: 8px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
}
.live-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--idp-ok);
box-shadow: 0 0 8px var(--idp-ok);
}
.wrap {
max-width: 1240px;
margin: 0 auto;
padding: 0 32px;
}
.proof, .section, .manifesto, .cta, footer {
border-bottom: 1px solid var(--idp-border-soft);
}
.proof {
padding: 56px 0;
}
.proof-label {
margin-bottom: 28px;
color: var(--idp-muted-fg);
text-align: center;
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.proof-row {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 24px;
place-items: center;
opacity: 0.72;
}
.proof-name {
color: var(--idp-fg-3);
font-family: var(--idp-display);
font-size: 18px;
font-weight: 700;
letter-spacing: -0.02em;
}
.section {
padding: 120px 0;
}
.section.alt {
background: var(--idp-bg-2);
}
.section-head {
max-width: 760px;
margin: 0 auto 64px;
text-align: center;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.eyebrow::before, .eyebrow::after {
content: '';
width: 24px;
height: 1px;
background: var(--idp-border-strong);
}
h2, h3, q {
margin: 0;
font-family: var(--idp-display);
letter-spacing: -0.03em;
}
h2 {
font-size: clamp(36px, 4.5vw, 56px);
line-height: 1.05;
}
em {
color: var(--idp-accent-hover);
font-family: var(--idp-serif);
font-style: italic;
font-weight: 400;
}
.lede {
max-width: 640px;
margin: 20px auto 0;
color: var(--idp-fg-3);
font-size: 17px;
line-height: 1.55;
}
.bento {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 16px;
}
.tile, .tier, .chain-panel, .terminal {
border: 1px solid var(--idp-border-soft);
border-radius: 12px;
background: var(--idp-bg-2);
}
.tile {
padding: 28px;
}
.tile.col-2 { grid-column: span 2; }
.tile.col-3 { grid-column: span 3; }
.tile.tall { grid-row: span 2; }
.tile-tag {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 14px;
color: var(--idp-accent-hover);
font-family: var(--idp-mono);
font-size: 10.5px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.tile-tag::before {
content: '';
width: 6px;
height: 6px;
border-radius: 999px;
background: var(--idp-accent);
}
.tile h3 {
margin-bottom: 10px;
font-size: 24px;
line-height: 1.15;
}
.tile p, .tier li, .chain-step p {
color: var(--idp-fg-3);
font-size: 14px;
line-height: 1.55;
}
.approval-stack {
display: grid;
gap: 6px;
margin-top: 22px;
}
.approval-row {
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 12px;
align-items: center;
padding: 10px 12px;
border: 1px solid var(--idp-border-soft);
border-left: 2px solid var(--idp-accent);
border-radius: 6px;
background: var(--idp-bg);
}
.avatar {
width: 28px;
height: 28px;
display: grid;
place-items: center;
border: 1px solid var(--idp-border);
border-radius: 50%;
background: var(--idp-card-2);
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 700;
}
.approval-row strong {
display: block;
color: var(--idp-fg);
font-size: 12.5px;
font-weight: 500;
}
.approval-row span.meta {
color: color-mix(in srgb, var(--idp-muted-fg), transparent 30%);
font-family: var(--idp-mono);
font-size: 10.5px;
}
.identity-card {
position: relative;
overflow: hidden;
margin-top: 22px;
border: 1px solid var(--idp-border);
border-radius: 10px;
padding: 20px;
background: linear-gradient(140deg, #1a1a1a 0%, #0a0a0a 100%);
}
.identity-card::after {
content: '';
position: absolute;
top: -100px;
right: -80px;
width: 240px;
height: 240px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0,105,242,0.4), transparent 65%);
}
.identity-card > * {
position: relative;
z-index: 1;
}
.chip {
width: 32px;
height: 24px;
margin: 18px 0 14px;
border-radius: 3px;
background: linear-gradient(135deg, #93bbfd 0%, #0050b9 80%);
}
.mono {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10.5px;
}
.metric {
margin-top: 8px;
background: linear-gradient(180deg, var(--idp-fg) 0%, var(--idp-muted-fg) 110%);
background-clip: text;
color: transparent;
font-family: var(--idp-display);
font-size: 64px;
font-weight: 700;
letter-spacing: -0.04em;
line-height: 1;
}
.metric span {
font-size: 24px;
}
.devices-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 22px;
}
.dev-cell {
padding: 14px 10px;
border: 1px solid var(--idp-border-soft);
border-radius: 6px;
background: var(--idp-bg);
text-align: center;
}
.dev-icon {
width: 32px;
height: 32px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
border: 1px solid rgba(96,165,250,0.35);
border-radius: 9px;
color: var(--idp-accent-hover);
}
.dev-name {
color: var(--idp-fg);
font-size: 12px;
font-weight: 500;
}
.dev-sub {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10px;
}
.chain-grid, .dev-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.chain-steps {
display: grid;
gap: 28px;
}
.chain-step {
display: grid;
grid-template-columns: 70px 1fr;
gap: 16px;
padding-bottom: 24px;
border-bottom: 1px solid var(--idp-border-soft);
}
.chain-step > div {
color: var(--idp-accent-hover);
font-family: var(--idp-mono);
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
}
.chain-panel {
overflow: hidden;
background: var(--idp-bg);
}
.chain-head {
display: flex;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid var(--idp-border-soft);
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.chain-block {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 12px;
margin: 8px 14px;
padding: 14px 16px;
border: 1px solid var(--idp-border-soft);
border-radius: 8px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
}
.chain-block.idp {
border-left: 2px solid var(--idp-accent);
background: rgba(0,80,185,0.08);
}
.tiers {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.tier {
position: relative;
display: flex;
flex-direction: column;
padding: 28px;
}
.tier.featured {
border-color: var(--idp-accent);
background: linear-gradient(180deg, rgba(59,130,246,0.06) 0%, var(--idp-bg-2) 40%);
}
.tier-name {
margin-bottom: 10px;
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.price {
margin: 12px 0 20px;
font-family: var(--idp-display);
font-size: 40px;
font-weight: 700;
}
.tier ul {
flex: 1;
margin: 0 0 24px;
padding-left: 20px;
}
.dev-text p {
color: var(--idp-fg-3);
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin: 18px 0 22px;
}
.tags span {
padding: 4px 10px;
border: 1px solid var(--idp-border);
border-radius: 999px;
background: var(--idp-bg-2);
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 10.5px;
}
.terminal {
overflow: hidden;
background: var(--idp-bg);
}
.term-bar {
display: flex;
gap: 6px;
padding: 10px 14px;
border-bottom: 1px solid var(--idp-border-soft);
}
.tdot {
width: 11px;
height: 11px;
border-radius: 50%;
}
.red { background: #ff5f57; }
.yellow { background: #ffbd2e; }
.green { background: #28c840; }
pre {
min-height: 300px;
margin: 0;
padding: 22px 24px;
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 13px;
line-height: 1.85;
}
.manifesto, .cta {
padding: 120px 0;
text-align: center;
}
q {
display: block;
max-width: 980px;
margin: 0 auto;
font-family: var(--idp-serif);
font-size: clamp(32px, 4vw, 48px);
font-style: italic;
line-height: 1.2;
quotes: none;
}
q::before, q::after {
content: none;
}
.cta {
position: relative;
overflow: hidden;
}
.cta::before {
content: '';
position: absolute;
left: 50%;
top: 50%;
width: 800px;
height: 600px;
transform: translate(-50%, -50%);
background: radial-gradient(ellipse, rgba(59,130,246,0.15) 0%, transparent 60%);
}
.cta .wrap {
position: relative;
}
.cta p {
max-width: 560px;
margin: 24px auto 32px;
color: var(--idp-fg-3);
}
footer {
padding: 64px 0 28px;
border-bottom: 0;
}
.footer-cols {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 48px;
margin-bottom: 48px;
}
.footer-brand p, .footer-col a, .footer-bottom {
color: var(--idp-muted-fg);
font-size: 13px;
}
.footer-col {
display: grid;
gap: 9px;
}
.footer-col h4 {
margin: 0;
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.footer-bottom {
display: flex;
justify-content: space-between;
padding-top: 22px;
border-top: 1px solid var(--idp-border-soft);
font-family: var(--idp-mono);
font-size: 11px;
}
@media (max-width: 1100px) {
.links { display: none; }
.bento { grid-template-columns: repeat(2, 1fr); }
.tile.col-2, .tile.col-3 { grid-column: span 2; }
.chain-grid, .dev-grid { grid-template-columns: 1fr; }
.tiers { grid-template-columns: 1fr; max-width: 520px; margin: 0 auto; }
.footer-cols { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 720px) {
nav, .wrap { padding-left: 20px; padding-right: 20px; }
.status, .actions .ghost { display: none; }
.proof-row { grid-template-columns: repeat(2, 1fr); }
.bento { grid-template-columns: 1fr; }
.tile.col-2, .tile.col-3 { grid-column: span 1; }
.section, .manifesto, .cta { padding: 80px 0; }
.footer-cols { grid-template-columns: 1fr; }
}
`,
];
private renderNav() {
return html`
<div class="nav-shell">
<nav>
<div class="logo">idp<span class="logo-dot"></span>global</div>
<div class="links"><a href="#product">Product</a><a href="#features">Features</a><a href="#chain">On-chain</a><a href="#pricing">Pricing</a><a href="#developers">Developers</a></div>
<div class="actions"><span class="status"><span class="live-dot"></span>All systems normal</span><idp-button variant="ghost" size="sm">Sign in</idp-button><idp-button variant="accent" size="sm">Claim identity</idp-button></div>
</nav>
</div>
`;
}
private renderFeatures() {
return html`
<section class="section" id="features">
<div class="wrap">
<div class="section-head"><div class="eyebrow">Capabilities</div><h2>Native on every screen <em>you already carry.</em></h2><p class="lede">Approvals on iPhone. Tap-to-auth via NFC. Lock-screen actions on Apple Watch. The same identity, one tap away on any device.</p></div>
<div class="bento">
<div class="tile col-3 tall"><div class="tile-tag">Push approvals</div><h3>Approve or deny <em>in one tap.</em></h3><p>Every login, OAuth grant, and sensitive action triggers a real-time approval.</p><div class="approval-stack">${['GitHub OAuth|repo:read - 2 min ago|approved|ok', 'CLI login - MacBook Pro|Berlin - just now|pending|accent', 'Unknown device|Lagos - 1 hr ago|denied|error', 'NFC tap - door 4F|HQ - 12 min ago|approved|ok'].map((rowArg) => { const row = rowArg.split('|'); return html`<div class="approval-row"><div class="avatar">${row[0].slice(0,2).toUpperCase()}</div><div><strong>${row[0]}</strong><span class="meta">${row[1]}</span></div><idp-badge variant=${row[3] as any}>${row[2]}</idp-badge></div>`; })}</div></div>
<div class="tile col-3"><div class="tile-tag">NFC tap-to-auth</div><h3>Tap to <em>authenticate.</em></h3><p>Hold your phone to any compatible reader. Identity token exchanges in under a second.</p><div class="identity-card"><h3>Alex Mercer</h3><div class="mono">@alexmercer - Personal</div><div class="chip"></div><div class="mono">did:idp:0x4a3f...c819</div></div></div>
<div class="tile col-3"><div class="tile-tag">Four platforms</div><h3>iPhone, Watch, iPad, Mac.</h3><p>Every device you carry is a trusted authenticator.</p><div class="devices-row">${[
['iPhone', 'phone'],
['Watch', 'smartphone-nfc'],
['iPad', 'device'],
['Mac', 'monitor'],
].map((deviceArg) => html`<div class="dev-cell"><div class="dev-icon"><idp-icon name=${deviceArg[1] as any} size="18"></idp-icon></div><div class="dev-name">${deviceArg[0]}</div><div class="dev-sub">trusted</div></div>`)}</div><div class="mono" style="margin-top:14px">One approval, anywhere - synchronized end-to-end.</div></div>
<div class="tile col-2"><div class="tile-tag">Average approval</div><h3>Sub-second auth.</h3><p>Push delivery, biometric prompt, and signed response under a second.</p><div class="metric">0.8<span>sec</span></div></div>
<div class="tile col-2"><div class="tile-tag">Audit-grade</div><h3>Every action, <em>on the record.</em></h3><p>Tamper-evident audit trail per identity and organization.</p></div>
<div class="tile col-2"><div class="tile-tag">Recovery</div><h3>Lose a phone? Not your identity.</h3><p>Multi-device recovery or social-recovery quorum. No vendor lockout.</p></div>
</div>
</div>
</section>
`;
}
private renderChain() {
return html`
<section class="section alt" id="chain"><div class="wrap"><div class="section-head"><div class="eyebrow">Cardano-anchored</div><h2>Your identity outlives <em>any single server.</em></h2><p class="lede">Every identity is anchored to the Cardano mainnet, independently verifiable and recoverable.</p></div><div class="chain-grid"><div class="chain-steps">${[['01 / 03', 'Immutable record', 'Your identity hash is written to Cardano at creation and on every key rotation.'], ['02 / 03', 'Synced on every change', 'Profile updates, device additions, and revocations are anchored to the chain.'], ['03 / 03', 'Independently verifiable', 'Any compatible resolver can verify your identity directly against the public ledger.']].map((stepArg) => html`<div class="chain-step"><div>${stepArg[0]}</div><section><h3>${stepArg[1]}</h3><p>${stepArg[2]}</p></section></div>`)}</div><div class="chain-panel"><div class="chain-head"><span>Cardano mainnet</span><idp-badge variant="accent">live</idp-badge></div>${['#9 841 220', '#9 841 221', '#9 841 222', '#9 841 223'].map((blockArg, indexArg) => html`<div class="chain-block ${indexArg === 1 || indexArg === 2 ? 'idp' : ''}"><span>${blockArg}</span><span>${indexArg === 1 ? 'did:idp:0x4a3f...c819' : indexArg === 2 ? 'did:idp:0x9b12...f034' : 'confirmed block'}</span><strong>${indexArg === 1 || indexArg === 2 ? 'idp.global' : 'confirmed'}</strong></div>`)}</div></div></div></section>
`;
}
private renderPricing() {
const tiers = [
['Personal', 'For one person.', '$0', ['One portable identity', 'Push approval on devices', 'NFC tap-to-authenticate', 'Anchored on Cardano'], 'Claim your identity'],
['Family & Org', 'For teams under 1,000.', '$0', ['Multi-member organization', 'Role-based access control', 'Shared OAuth client registry', 'Full audit trail'], 'Start an organization'],
['Enterprise', 'Above $1M ARR.', 'Fair', ['Self-hosted and air-gap deployable', 'Compliance and audit support', 'Global admin across orgs', 'Priority SLA'], 'Talk to us'],
];
return html`<section class="section" id="pricing"><div class="wrap"><div class="section-head"><div class="eyebrow">Pricing</div><h2>The same identity, <em>at every scale.</em></h2><p class="lede">Free for the first thousand users. Fair contribution above that. No hard paywalls.</p></div><div class="tiers">${tiers.map((tierArg, indexArg) => html`<div class="tier ${indexArg === 1 ? 'featured' : ''}"><div class="tier-name">${tierArg[0]}</div><h3>${tierArg[1]}</h3><div class="price">${tierArg[2]}</div><ul>${(tierArg[3] as string[]).map((itemArg) => html`<li>${itemArg}</li>`)}</ul><idp-button variant=${indexArg === 1 ? 'accent' : 'ghost'}>${tierArg[4]}</idp-button></div>`)}</div></div></section>`;
}
private renderDevelopers() {
return html`
<section class="section alt" id="developers"><div class="wrap dev-grid"><div class="dev-text"><div class="eyebrow">For developers</div><h2>No black boxes <em>in your identity stack.</em></h2><p>idp.global is fully open source and MIT licensed. Read the cryptography. Verify the Cardano sync. Run it on your own metal.</p><div class="tags">${['MIT licensed', 'OAuth 2 / OIDC', 'Self-hostable', 'Air-gappable', 'Cardano native', 'SOC 2'].map((tagArg) => html`<span>${tagArg}</span>`)}</div><idp-button variant="accent">View source</idp-button></div><div class="terminal"><div class="term-bar"><span class="tdot red"></span><span class="tdot yellow"></span><span class="tdot green"></span></div><pre><code>$ idp identity create
OK Identity created - did:idp:0x4a3f...c819
OK Confirmed on-chain - permanent
$ idp login github.com
OK Push sent - iPhone 15 Pro
OK Approved - Watch - 0.8s</code></pre></div></div></section>
`;
}
public render(): TemplateResult {
return html`
<div class="page">
${this.renderNav()}
<idp-landing-hero></idp-landing-hero>
<section class="proof"><div class="wrap"><div class="proof-label">Built for identity at every scale</div><div class="proof-row">${['Open Source', 'Self-hostable', 'Cardano anchored', 'OIDC ready', 'Passkey first', 'Free for everyone'].map((nameArg) => html`<div class="proof-name">${nameArg}</div>`)}</div></div></section>
${this.renderFeatures()}${this.renderChain()}${this.renderPricing()}${this.renderDevelopers()}
<section class="manifesto"><div class="wrap"><div class="eyebrow">Why we built this</div><q>Identity should not be a product the user is sold.<br/>It should be a permanent <em>fact</em>, owned by the person it describes.</q></div></section>
<section class="cta"><div class="wrap"><h2>Claim your identity.<br/><em>Free, forever.</em></h2><p>Sixty seconds to claim, anchored to Cardano on submission. No credit card. No vendor lock-in.</p><idp-button variant="accent" size="lg">Claim your identity</idp-button></div></section>
<footer><div class="wrap"><div class="footer-cols"><div class="footer-brand"><div class="logo">idp<span class="logo-dot"></span>global</div><p>An open identity provider for everyone. Anchored on Cardano. Built in the open. Yours forever.</p></div><div class="footer-col"><h4>Product</h4><a>For individuals</a><a>For organizations</a><a>Cardano sync</a></div><div class="footer-col"><h4>Developers</h4><a>Documentation</a><a>Self-hosting</a><a>SDKs</a></div><div class="footer-col"><h4>Company</h4><a>Manifesto</a><a>Security</a><a>Privacy</a></div></div><div class="footer-bottom"><span>© 2026 idp.global - MIT - Anchored to Cardano</span><span>Source - Community</span></div></div></footer>
</div>
`;
}
}
+137
View File
@@ -0,0 +1,137 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
declare global {
interface HTMLElementTagNameMap {
'idp-mobile-frame': IdpMobileFrame;
}
}
@customElement('idp-mobile-frame')
export class IdpMobileFrame extends DeesElement {
public static demo = () => html`
<idp-mobile-frame>
<div style="height: 100%; background: #fff; padding: 72px 20px 20px; box-sizing: border-box;">Screen content</div>
</idp-mobile-frame>
`;
public static demoGroups = ['idp.global v3 device frames'];
@property({ type: String })
public accessor time = '9:41';
@property({ type: Boolean, reflect: true })
public accessor dark = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: inline-block;
}
.device {
position: relative;
width: 402px;
height: 874px;
max-width: 100%;
border-radius: 48px;
overflow: hidden;
background: var(--idp-bg);
box-shadow: 0 40px 80px rgb(0 0 0 / 0.18), 0 0 0 1px rgb(0 0 0 / 0.12);
}
.island {
position: absolute;
top: 11px;
left: 50%;
z-index: 50;
width: 126px;
height: 37px;
transform: translateX(-50%);
border-radius: 24px;
background: #000;
}
.status {
position: absolute;
inset: 0 0 auto;
z-index: 40;
height: 58px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 28px 0;
box-sizing: border-box;
color: var(--idp-fg);
font-size: 15px;
font-weight: 650;
pointer-events: none;
}
.status-icons {
display: inline-flex;
align-items: center;
gap: 7px;
}
.bar {
width: 4px;
border-radius: 999px;
background: currentColor;
}
.screen {
height: 100%;
overflow: hidden;
}
.home {
position: absolute;
left: 0;
right: 0;
bottom: 0;
z-index: 60;
height: 34px;
display: flex;
align-items: flex-end;
justify-content: center;
padding-bottom: 8px;
pointer-events: none;
}
.home::before {
content: '';
width: 139px;
height: 5px;
border-radius: 100px;
background: color-mix(in srgb, var(--idp-fg), transparent 72%);
}
@media (max-width: 520px) {
:host {
display: block;
width: 100%;
}
.device {
width: 100%;
height: auto;
aspect-ratio: 402 / 874;
border-radius: 0;
}
.island {
display: none;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="device">
<div class="island"></div>
<div class="status">
<span>${this.time}</span>
<span class="status-icons" aria-hidden="true">
<span class="bar" style="height: 5px"></span>
<span class="bar" style="height: 8px"></span>
<span class="bar" style="height: 11px"></span>
<span style="width: 24px; height: 12px; border: 1.5px solid currentColor; border-radius: 4px;"></span>
</span>
</div>
<div class="screen"><slot></slot></div>
<div class="home"></div>
</div>
`;
}
}
+287
View File
@@ -0,0 +1,287 @@
import { DeesElement, html, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-button.js';
import './idp-icon.js';
import './idp-inbox-preview.js';
import './idp-mobile-frame.js';
declare global {
interface HTMLElementTagNameMap {
'idp-mobile-showcase': IdpMobileShowcase;
}
}
@customElement('idp-mobile-showcase')
export class IdpMobileShowcase extends DeesElement {
public static demo = () => html`<idp-mobile-showcase></idp-mobile-showcase>`;
public static demoGroups = ['idp.global v3 full pages'];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.showcase {
min-height: 100vh;
padding: 56px;
background: radial-gradient(circle at 1px 1px, rgba(0,0,0,0.08) 1px, transparent 0) 0 0 / 24px 24px, #fafafa;
color: #09090b;
}
.head {
max-width: 1180px;
margin: 0 auto 44px;
}
.badge {
display: inline-flex;
align-items: center;
gap: 6px;
margin-bottom: 16px;
padding: 4px 10px;
border: 1px solid #e4e4e7;
border-radius: 999px;
background: #fff;
color: #52525b;
font-size: 11px;
font-weight: 500;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #16a34a;
}
h1 {
max-width: 900px;
margin: 0 0 14px;
font-family: var(--idp-display);
font-size: clamp(36px, 5vw, 56px);
font-weight: 750;
letter-spacing: -0.035em;
line-height: 1.05;
}
p {
max-width: 680px;
margin: 0;
color: #52525b;
font-size: 16px;
line-height: 1.55;
}
.tokens {
display: flex;
flex-wrap: wrap;
gap: 24px;
margin-top: 24px;
padding: 16px;
border: 1px solid #e4e4e7;
border-radius: 12px;
background: #fff;
}
.token-label {
margin-bottom: 4px;
color: #71717a;
font-size: 10px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.token-value {
display: flex;
align-items: center;
gap: 8px;
color: #18181b;
font-size: 13px;
font-weight: 550;
}
.swatch {
width: 18px;
height: 18px;
border-radius: 5px;
background: var(--swatch);
border: 1px solid #e4e4e7;
}
.section {
max-width: 1180px;
margin: 0 auto 56px;
}
.section-title {
margin-bottom: 18px;
color: #71717a;
font-family: var(--idp-mono);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.phones {
display: grid;
grid-template-columns: repeat(2, minmax(300px, 402px));
gap: 28px;
align-items: start;
}
.multi {
display: grid;
grid-template-columns: 280px minmax(0, 1fr) 360px;
gap: 28px;
align-items: start;
}
.watch, .ipad, .mac {
border: 1px solid #e4e4e7;
background: #fff;
box-shadow: 0 20px 50px rgba(0,0,0,0.08);
}
.watch {
width: 236px;
height: 286px;
display: grid;
place-items: center;
border-radius: 60px;
background: #09090b;
color: #fafafa;
}
.watch-screen {
width: 178px;
display: grid;
gap: 12px;
text-align: center;
}
.watch-app {
color: #a1a1aa;
font-family: var(--idp-mono);
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.watch-title {
font-size: 18px;
font-weight: 700;
}
.watch-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.watch-actions button {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 5px;
height: 34px;
border: 1px solid #27272a;
border-radius: 999px;
background: #18181b;
color: #fafafa;
}
.watch-actions .approve {
background: #3b82f6;
border-color: #3b82f6;
}
.ipad {
min-height: 520px;
overflow: hidden;
border-radius: 26px;
}
.ipad-shell {
display: grid;
grid-template-columns: 220px 1fr;
min-height: 520px;
}
.ipad-sidebar {
padding: 18px;
border-right: 1px solid #e4e4e7;
background: #f8f8f7;
}
.ipad-main {
padding: 22px;
}
.ipad-card {
display: flex;
align-items: flex-start;
gap: 10px;
margin-top: 16px;
padding: 16px;
border: 1px solid #e4e4e7;
border-radius: 12px;
}
.mac {
overflow: hidden;
border-radius: 14px;
}
.mac-bar {
display: flex;
gap: 6px;
padding: 11px 14px;
border-bottom: 1px solid #e4e4e7;
}
.tdot {
width: 10px;
height: 10px;
border-radius: 50%;
}
.mac-body {
display: grid;
gap: 12px;
padding: 18px;
}
.mac-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 12px;
border: 1px solid #e4e4e7;
border-radius: 10px;
}
.row-label {
display: inline-flex;
align-items: center;
gap: 8px;
}
@media (max-width: 1120px) {
.multi {
grid-template-columns: 1fr;
}
}
@media (max-width: 900px) {
.showcase {
padding: 28px;
}
.phones {
grid-template-columns: 1fr;
}
}
`,
];
public render(): TemplateResult {
return html`
<div class="showcase">
<header class="head">
<div class="badge"><span class="dot"></span>Mobile redesign - v3 - shadcn tokens</div>
<h1>A personal identity provider, built across every device you carry.</h1>
<p>Same four-platform architecture: flat surfaces, 1px borders, neutral palette, and blue accent used only where action is expected.</p>
<div class="tokens">
${[
['Primary', '#18181b', 'zinc-900'],
['Accent', '#0050b9', 'idp blue'],
['Muted', '#f4f4f2', 'paper muted'],
['Border', '#e4e4e7', 'zinc-200'],
].map((tokenArg) => html`<div><div class="token-label">${tokenArg[0]}</div><div class="token-value"><span class="swatch" style="--swatch:${tokenArg[1]}"></span>${tokenArg[2]}</div></div>`)}
</div>
</header>
<section class="section">
<div class="section-title">iPhone</div>
<div class="phones"><idp-inbox-preview></idp-inbox-preview><idp-inbox-preview dark></idp-inbox-preview></div>
</section>
<section class="section">
<div class="section-title">Watch, iPad, Mac</div>
<div class="multi">
<div class="watch"><div class="watch-screen"><div class="watch-app">idp.global</div><idp-icon name="shield" size="28" style="margin:0 auto;color:#60a5fa"></idp-icon><div class="watch-title">GitHub wants access</div><div style="color:#a1a1aa;font-size:12px;">repo:read - Berlin</div><div class="watch-actions"><button><idp-icon name="x" size="13"></idp-icon>Deny</button><button class="approve"><idp-icon name="check" size="13"></idp-icon>Approve</button></div></div></div>
<div class="ipad"><div class="ipad-shell"><aside class="ipad-sidebar"><strong>Inbox</strong><p>3 pending approvals</p><div class="ipad-card"><idp-icon name="globe" size="16"></idp-icon><div><strong>GitHub OAuth</strong><br/><span style="color:#71717a">repo:read - now</span></div></div><div class="ipad-card"><idp-icon name="cloud" size="16"></idp-icon><div><strong>Hetzner Cloud</strong><br/><span style="color:#71717a">new network - 8m</span></div></div></aside><main class="ipad-main"><h2>Approval detail</h2><p>Full context before a sensitive action is approved.</p><div class="ipad-card"><idp-icon name="laptop" size="16"></idp-icon><div><strong>Device</strong><br/>MacBook Pro - Safari - Berlin, DE</div></div><div class="ipad-card"><idp-icon name="key" size="16"></idp-icon><div><strong>Requested scopes</strong><br/>openid, profile, email, repo:read</div></div></main></div></div>
<div class="mac"><div class="mac-bar"><span class="tdot" style="background:#ff5f57"></span><span class="tdot" style="background:#ffbd2e"></span><span class="tdot" style="background:#28c840"></span></div><div class="mac-body"><strong>Menu bar approvals</strong><div class="mac-row"><span class="row-label"><idp-icon name="globe" size="15"></idp-icon>GitHub OAuth</span><idp-button variant="accent" size="sm" icon="check">Approve</idp-button></div><div class="mac-row"><span class="row-label"><idp-icon name="nfc" size="15"></idp-icon>NFC tap - door 4F</span><idp-button variant="ghost" size="sm" icon="chevron-right">Review</idp-button></div><div class="mac-row"><span class="row-label"><idp-icon name="key" size="15"></idp-icon>Key rotation</span><idp-button variant="ghost" size="sm" icon="shield">Confirm</idp-button></div></div></div>
</div>
</section>
</div>
`;
}
}
+77
View File
@@ -0,0 +1,77 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
declare global {
interface HTMLElementTagNameMap {
'idp-toggle': IdpToggle;
}
}
@customElement('idp-toggle')
export class IdpToggle extends DeesElement {
public static demo = () => html`<idp-toggle checked></idp-toggle>`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: Boolean, reflect: true })
public accessor checked = false;
@property({ type: Boolean, reflect: true })
public accessor disabled = false;
public static styles = [
...idpElementStyles,
css`
:host {
display: inline-flex;
}
button {
width: 36px;
height: 20px;
border: none;
border-radius: 999px;
padding: 2px;
background: var(--idp-border);
cursor: pointer;
transition: background 150ms ease;
}
:host([checked]) button {
background: var(--idp-accent);
}
:host([disabled]) button {
opacity: 0.5;
cursor: not-allowed;
}
.knob {
width: 16px;
height: 16px;
border-radius: 999px;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 0.22);
transition: transform 150ms ease;
}
:host([checked]) .knob {
transform: translateX(16px);
}
`,
];
private toggle() {
if (this.disabled) {
return;
}
this.checked = !this.checked;
this.dispatchEvent(new CustomEvent('idp-toggle-change', {
detail: { checked: this.checked },
bubbles: true,
composed: true,
}));
}
public render(): TemplateResult {
return html`
<button role="switch" aria-checked=${this.checked} ?disabled=${this.disabled} @click=${this.toggle}>
<div class="knob"></div>
</button>
`;
}
}
+15
View File
@@ -0,0 +1,15 @@
export * from './tokens.js';
export * from './idp-icon.js';
export * from './idp-button.js';
export * from './idp-badge.js';
export * from './idp-card.js';
export * from './idp-input.js';
export * from './idp-toggle.js';
export * from './idp-approval-card.js';
export * from './idp-mobile-frame.js';
export * from './idp-inbox-preview.js';
export * from './idp-dashboard-window.js';
export * from './idp-admin-shell.js';
export * from './idp-landing-hero.js';
export * from './idp-landing-page.js';
export * from './idp-mobile-showcase.js';
+120
View File
@@ -0,0 +1,120 @@
import { css, cssManager } from '@design.estate/dees-element';
export const idpAccent = '#3B82F6';
export const idpTheme = {
bg: 'var(--idp-bg)',
fg: 'var(--idp-fg)',
muted: 'var(--idp-muted)',
mutedFg: 'var(--idp-muted-fg)',
border: 'var(--idp-border)',
card: 'var(--idp-card)',
primary: 'var(--idp-primary)',
primaryFg: 'var(--idp-primary-fg)',
accent: 'var(--idp-accent)',
accentHover: 'var(--idp-accent-hover)',
accentSoft: 'var(--idp-accent-soft)',
info: 'var(--idp-info)',
destructive: 'var(--idp-destructive)',
ok: 'var(--idp-ok)',
warn: 'var(--idp-warn)',
radius: 'var(--idp-radius)',
};
export const idpBaseStyles = css`
:host {
--idp-bg: #fafaf9;
--idp-bg-2: #f4f4f2;
--idp-fg: #0a0a0a;
--idp-fg-2: #3f3f46;
--idp-fg-3: #52525b;
--idp-muted: #f1f1ef;
--idp-muted-fg: #71717a;
--idp-border: #e4e4e7;
--idp-border-soft: #ededec;
--idp-border-strong: #d4d4d8;
--idp-card: #ffffff;
--idp-card-2: #f8f8f7;
--idp-primary: #18181b;
--idp-primary-fg: #fafafa;
--idp-accent: #0050b9;
--idp-accent-hover: #0069f2;
--idp-accent-fg: #ffffff;
--idp-accent-soft: #e6effb;
--idp-destructive: #ef4444;
--idp-ok: #16a34a;
--idp-ok-bg: #f0fdf4;
--idp-ok-border: #bbf7d0;
--idp-warn: #b45309;
--idp-warn-bg: #fef9c3;
--idp-warn-border: #fde68a;
--idp-error: #dc2626;
--idp-error-bg: #fef2f2;
--idp-error-border: #fecaca;
--idp-info: #1e40af;
--idp-info-bg: #eff6ff;
--idp-info-border: #bfdbfe;
--idp-chart-1: #0050b9;
--idp-chart-2: #16a34a;
--idp-chart-3: #dc2626;
--idp-chart-4: #b45309;
--idp-chart-5: #6e5be6;
--idp-spark-up: #16a34a;
--idp-spark-down: #dc2626;
--idp-spark-info: #0050b9;
--idp-radius: 8px;
--idp-radius-lg: 12px;
--idp-font: 'Geist', ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--idp-display: 'Plus Jakarta Sans', 'Geist', ui-sans-serif, system-ui, sans-serif;
--idp-mono: 'Intel One Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
--idp-serif: 'Instrument Serif', Georgia, serif;
font-family: var(--idp-font);
font-feature-settings: "cv11", "ss03";
font-variant-numeric: tabular-nums;
-webkit-font-smoothing: antialiased;
}
:host([theme="dark"]),
:host([dark]) {
--idp-bg: #0a0a0a;
--idp-bg-2: #111111;
--idp-fg: #fafafa;
--idp-fg-2: #d4d4d8;
--idp-fg-3: hsl(0 0% 70%);
--idp-muted: #161616;
--idp-muted-fg: hsl(0 0% 55%);
--idp-border: #262626;
--idp-border-soft: #1c1c1c;
--idp-border-strong: #333333;
--idp-card: #121212;
--idp-card-2: #161616;
--idp-primary: #fafafa;
--idp-primary-fg: #18181b;
--idp-accent: #3b82f6;
--idp-accent-hover: #60a5fa;
--idp-accent-soft: rgba(59, 130, 246, 0.15);
--idp-destructive: #ef4444;
--idp-ok: #4ade80;
--idp-ok-bg: rgba(20, 83, 45, 0.4);
--idp-ok-border: rgba(74, 222, 128, 0.25);
--idp-warn: #fbbf24;
--idp-warn-bg: rgba(69, 26, 3, 0.6);
--idp-warn-border: rgba(251, 191, 36, 0.25);
--idp-error: #f87171;
--idp-error-bg: rgba(69, 10, 10, 0.6);
--idp-error-border: rgba(248, 113, 113, 0.25);
--idp-info: #93bbfd;
--idp-info-bg: rgba(59, 130, 246, 0.15);
--idp-info-border: rgba(59, 130, 246, 0.3);
--idp-chart-1: #3b82f6;
--idp-chart-2: #4ade80;
--idp-chart-3: #f87171;
--idp-chart-4: #fbbf24;
--idp-chart-5: #a78bfa;
--idp-spark-up: #4ade80;
--idp-spark-down: #f87171;
--idp-spark-info: #93bbfd;
}
`;
export const idpElementStyles = [cssManager.defaultStyles, idpBaseStyles];
+1
View File
@@ -0,0 +1 @@
export * from './elements/index.js';
+26
View File
@@ -0,0 +1,26 @@
import { html } from '@design.estate/dees-element';
import '../elements/index.js';
export const LandingPage = () => html`
<idp-landing-page></idp-landing-page>
`;
export const AdminConsole = () => html`
<div style="box-sizing: border-box; min-height: 100vh; padding: 32px; background: #f4f4f2; font-family: Geist, system-ui, sans-serif;">
<idp-admin-shell></idp-admin-shell>
</div>
`;
export const MobileShowcase = () => html`
<idp-mobile-showcase></idp-mobile-showcase>
`;
export const ComposedViews = () => html`
<div style="display: grid; gap: 48px; background: #0a0a0a;">
<idp-landing-page></idp-landing-page>
<div style="padding: 48px; background: #f4f4f2;">
<idp-admin-shell></idp-admin-shell>
</div>
<idp-mobile-showcase></idp-mobile-showcase>
</div>
`;