feat(catalog): add initial idp.global component catalog with primitives, composed views, and full-page showcases
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
|||||||
|
.nogit/
|
||||||
|
|
||||||
|
# installs
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# builds
|
||||||
|
dist/
|
||||||
|
dist_*/
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.cache/
|
||||||
|
.rpt2_cache
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"@git.zone/cli": {
|
||||||
|
"projectType": "wcc",
|
||||||
|
"module": {
|
||||||
|
"githost": "code.foss.global",
|
||||||
|
"gitscope": "idp.global",
|
||||||
|
"gitrepo": "catalog",
|
||||||
|
"description": "Web component catalog for idp.global, based on the v2 product design language.",
|
||||||
|
"npmPackagename": "@idp.global/catalog",
|
||||||
|
"license": "MIT",
|
||||||
|
"projectDomain": "idp.global",
|
||||||
|
"keywords": [
|
||||||
|
"idp.global",
|
||||||
|
"web components",
|
||||||
|
"identity",
|
||||||
|
"authentication",
|
||||||
|
"design system"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"release": {
|
||||||
|
"registries": [
|
||||||
|
"https://verdaccio.lossless.digital",
|
||||||
|
"https://registry.npmjs.org"
|
||||||
|
],
|
||||||
|
"accessLevel": "public"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"@git.zone/tsbundle": {
|
||||||
|
"bundles": [
|
||||||
|
{
|
||||||
|
"from": "./ts_web/index.ts",
|
||||||
|
"to": "./dist_bundle/bundle.js",
|
||||||
|
"outputMode": "bundle",
|
||||||
|
"bundler": "esbuild",
|
||||||
|
"production": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"@git.zone/tswatch": {
|
||||||
|
"preset": "element"
|
||||||
|
},
|
||||||
|
"@ship.zone/szci": {}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 178 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 140 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
@@ -0,0 +1,13 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-05-03 - 1.1.0 - feat(catalog)
|
||||||
|
add initial idp.global component catalog with primitives, composed views, and full-page showcases
|
||||||
|
|
||||||
|
- Adds a new web component catalog package with reusable UI primitives such as buttons, badges, cards, inputs, toggles, and icons.
|
||||||
|
- Introduces composed identity surfaces including approval cards, inbox previews, mobile frames, dashboard windows, admin shell views, and landing hero sections.
|
||||||
|
- Includes full-page catalog demos and workspace setup for browsing landing, admin, and mobile showcase experiences.
|
||||||
|
- Configures package metadata, build tooling, documentation files, and bundled design assets for the initial 1.0.0 release.
|
||||||
|
|
||||||
|
## 1.0.0
|
||||||
|
|
||||||
|
- Initial idp.global v2 element catalog.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!--gitzone element-->
|
||||||
|
<!-- made by Task Venture Capital GmbH -->
|
||||||
|
<!-- checkout https://maintainedby.lossless.com for awesome OpenSource projects -->
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<!--Lets set some basic meta tags-->
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height"
|
||||||
|
/>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
|
||||||
|
<!--Lets load standard fonts-->
|
||||||
|
<link rel="preconnect" href="https://assetbroker.lossless.one/" crossorigin>
|
||||||
|
<link rel="stylesheet" href="https://assetbroker.lossless.one/fonts/fonts.css">
|
||||||
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%233b82f6' d='M12 2l8 3v7c0 6-8 10-8 10s-8-4-8-10V5l8-3z'/%3E%3C/svg%3E">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0px;
|
||||||
|
background: #222222;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script type="module" src="/bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// dees tools
|
||||||
|
import * as deesWccTools from '@design.estate/dees-wcctools';
|
||||||
|
import * as deesDomTools from '@design.estate/dees-domtools';
|
||||||
|
|
||||||
|
// elements and pages
|
||||||
|
import * as elements from '../ts_web/elements/index.js';
|
||||||
|
import * as pages from '../ts_web/pages/index.js';
|
||||||
|
|
||||||
|
const fullPageElementNames = new Set([
|
||||||
|
'IdpAdminShell',
|
||||||
|
'IdpLandingPage',
|
||||||
|
'IdpMobileShowcase',
|
||||||
|
]);
|
||||||
|
|
||||||
|
deesWccTools.setupWccTools({
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
name: 'Full Pages',
|
||||||
|
type: 'pages',
|
||||||
|
items: pages,
|
||||||
|
icon: 'web',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Composed Views',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
icon: 'dashboard',
|
||||||
|
filter: (nameArg) => fullPageElementNames.has(nameArg) || ['IdpDashboardWindow', 'IdpLandingHero', 'IdpInboxPreview'].includes(nameArg),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Primitives',
|
||||||
|
type: 'elements',
|
||||||
|
items: elements,
|
||||||
|
icon: 'category',
|
||||||
|
filter: (nameArg) => nameArg.startsWith('Idp') && !fullPageElementNames.has(nameArg) && !['IdpDashboardWindow', 'IdpLandingHero', 'IdpInboxPreview'].includes(nameArg),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
deesDomTools.elementBasic.setup();
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Task Venture Capital GmbH
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
{
|
||||||
|
"name": "@idp.global/catalog",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": false,
|
||||||
|
"description": "Web component catalog for idp.global, based on the v3 product design language.",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist_ts_web/index.js"
|
||||||
|
},
|
||||||
|
"main": "dist_ts_web/index.js",
|
||||||
|
"typings": "dist_ts_web/index.d.ts",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "pnpm run build",
|
||||||
|
"build": "tsbuild tsfolders --allowimplicitany && tsbundle",
|
||||||
|
"watch": "tswatch",
|
||||||
|
"buildDocs": "tsdoc"
|
||||||
|
},
|
||||||
|
"author": "Task Venture Capital GmbH",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@design.estate/dees-catalog": "^3.81.0",
|
||||||
|
"@design.estate/dees-domtools": "^2.5.4",
|
||||||
|
"@design.estate/dees-element": "^2.2.4",
|
||||||
|
"@design.estate/dees-wcctools": "^3.9.0",
|
||||||
|
"lucide": "^1.14.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@git.zone/tsbuild": "^4.4.0",
|
||||||
|
"@git.zone/tsbundle": "^2.10.0",
|
||||||
|
"@git.zone/tswatch": "^3.3.2",
|
||||||
|
"@push.rocks/projectinfo": "^5.1.0",
|
||||||
|
"@types/node": "^25.6.0"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"ts_web/**/*",
|
||||||
|
"dist/**/*",
|
||||||
|
"dist_*/**/*",
|
||||||
|
"dist_ts_web/**/*",
|
||||||
|
"assets/**/*",
|
||||||
|
"html/**/*",
|
||||||
|
"license",
|
||||||
|
"readme.md",
|
||||||
|
"changelog.md"
|
||||||
|
],
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+ssh://git@code.foss.global:29419/idp.global/catalog.git"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://code.foss.global/idp.global/catalog/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://code.foss.global/idp.global/catalog#readme",
|
||||||
|
"browserslist": [
|
||||||
|
"last 1 Chrome versions"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"idp.global",
|
||||||
|
"catalog",
|
||||||
|
"web components",
|
||||||
|
"identity",
|
||||||
|
"authentication",
|
||||||
|
"design system"
|
||||||
|
],
|
||||||
|
"packageManager": "pnpm@10.7.0+sha512.6b865ad4b62a1d9842b61d674a393903b8712034030524f652b8842c2b553c72176b278f64c463e52d40fff8aba385c235c8c9ecf5cc7de4fd78b8bb6d49633ab6"
|
||||||
|
}
|
||||||
Generated
+6665
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
# @idp.global/catalog
|
||||||
|
|
||||||
|
Web component catalog for idp.global, based on the v2 product design language.
|
||||||
|
|
||||||
|
The catalog converts the `idp.global v2.zip` design studies into reusable custom elements for landing pages, approval surfaces, account/admin shells, and mobile-style identity flows.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @idp.global/catalog
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import '@idp.global/catalog';
|
||||||
|
```
|
||||||
|
|
||||||
|
```html
|
||||||
|
<idp-landing-hero></idp-landing-hero>
|
||||||
|
<idp-approval-card app-name="GitHub" request-text="Sign in to github.com"></idp-approval-card>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Elements
|
||||||
|
|
||||||
|
- `idp-button` for v2 button variants.
|
||||||
|
- `idp-badge` for small status labels.
|
||||||
|
- `idp-card` for bordered shadcn-style content containers.
|
||||||
|
- `idp-input` for text/password/email inputs.
|
||||||
|
- `idp-toggle` for compact boolean controls.
|
||||||
|
- `idp-icon` for bundled monochrome identity icons.
|
||||||
|
- `idp-approval-card` for approve/deny request cards.
|
||||||
|
- `idp-inbox-preview` for mobile approval inbox previews.
|
||||||
|
- `idp-mobile-frame` for iOS-style product screenshots.
|
||||||
|
- `idp-admin-shell` for account/org admin layout previews.
|
||||||
|
- `idp-landing-hero` for the public landing hero section.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This package is component-only. It does not perform authentication, store sessions, or call the idp.global backend.
|
||||||
|
|
||||||
|
## Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
pnpm run build
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
@@ -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
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './elements/index.js';
|
||||||
@@ -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>
|
||||||
|
`;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"types": ["node"]
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"dist_*/**/*.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user