import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
type TemplateResult,
} from '@design.estate/dees-element';
import type { ISgToken, ISgOrganization, TSgProtocol } from '../interfaces.js';
declare global {
interface HTMLElementTagNameMap {
'sg-tokens-view': SgTokensView;
}
}
const ALL_PROTOCOLS: TSgProtocol[] = ['npm', 'oci', 'maven', 'cargo', 'composer', 'pypi', 'rubygems'];
@customElement('sg-tokens-view')
export class SgTokensView extends DeesElement {
public static demo = () => html`
`;
public static demoGroups = ['Auth'];
@property({ type: Array })
public accessor tokens: ISgToken[] = [];
@property({ type: Array })
public accessor organizations: ISgOrganization[] = [];
private showCreateForm = false;
private createName = '';
private createProtocols: TSgProtocol[] = [];
private createOrgId = '';
private createExpiryDays = 0;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
color: ${cssManager.bdTheme('#111', '#fff')};
}
.container {
display: flex;
flex-direction: column;
gap: 24px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.page-title {
font-size: 24px;
font-weight: 700;
letter-spacing: -0.02em;
}
.create-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: ${cssManager.bdTheme('#111', '#fff')};
border: none;
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#fff', '#111')};
cursor: pointer;
transition: opacity 150ms ease;
}
.create-btn:hover {
opacity: 0.85;
}
/* Create form */
.create-form {
background: ${cssManager.bdTheme('#fff', '#111')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
padding: 24px;
display: flex;
flex-direction: column;
gap: 16px;
}
.form-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
}
.form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.form-label {
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#111', '#ddd')};
text-transform: uppercase;
letter-spacing: 0.04em;
}
.form-input {
padding: 10px 12px;
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 14px;
color: ${cssManager.bdTheme('#111', '#fff')};
outline: none;
font-family: inherit;
}
.form-input:focus {
border-color: ${cssManager.bdTheme('#111', '#fff')};
}
.form-select {
padding: 10px 12px;
background: ${cssManager.bdTheme('#fff', '#0a0a0a')};
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 14px;
color: ${cssManager.bdTheme('#111', '#fff')};
outline: none;
font-family: inherit;
}
.protocol-selector {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.protocol-chip {
padding: 6px 12px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
transition: all 150ms ease;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.protocol-chip:hover {
border-color: ${cssManager.bdTheme('#999', '#666')};
}
.protocol-chip.selected {
background: ${cssManager.bdTheme('#111', '#fff')};
color: ${cssManager.bdTheme('#fff', '#111')};
border-color: ${cssManager.bdTheme('#111', '#fff')};
}
.form-actions {
display: flex;
gap: 8px;
margin-top: 8px;
}
.form-submit {
padding: 8px 20px;
background: ${cssManager.bdTheme('#111', '#fff')};
border: none;
font-size: 13px;
font-weight: 600;
color: ${cssManager.bdTheme('#fff', '#111')};
cursor: pointer;
transition: opacity 150ms ease;
}
.form-submit:hover {
opacity: 0.85;
}
.form-cancel {
padding: 8px 20px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 13px;
color: ${cssManager.bdTheme('#666', '#999')};
cursor: pointer;
transition: all 150ms ease;
}
.form-cancel:hover {
border-color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#111', '#fff')};
}
/* Token list */
.token-list {
display: flex;
flex-direction: column;
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.token-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: ${cssManager.bdTheme('#fff', '#111')};
border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.token-row:last-child {
border-bottom: none;
}
.token-info {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
}
.token-name-row {
display: flex;
align-items: center;
gap: 8px;
}
.token-name {
font-size: 14px;
font-weight: 600;
color: ${cssManager.bdTheme('#111', '#fff')};
}
.token-prefix {
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: ${cssManager.bdTheme('#888', '#777')};
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
padding: 1px 6px;
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.token-protocols {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.token-protocol {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
padding: 1px 6px;
background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')};
color: ${cssManager.bdTheme('#666', '#aaa')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.token-meta {
display: flex;
gap: 12px;
font-size: 12px;
color: ${cssManager.bdTheme('#888', '#777')};
flex-wrap: wrap;
}
.token-expired {
color: #ef4444;
font-weight: 600;
}
.token-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.revoke-btn {
padding: 6px 14px;
background: transparent;
border: 1px solid rgba(239, 68, 68, 0.3);
font-size: 12px;
font-weight: 500;
color: #ef4444;
cursor: pointer;
transition: all 150ms ease;
}
.revoke-btn:hover {
background: rgba(239, 68, 68, 0.15);
}
.empty-state {
text-align: center;
padding: 48px 32px;
font-size: 14px;
color: ${cssManager.bdTheme('#888', '#777')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
background: ${cssManager.bdTheme('#fff', '#111')};
}
`,
];
public render(): TemplateResult {
return html`
${this.showCreateForm ? this.renderCreateForm() : ''}
${this.tokens.length > 0
? html`
${this.tokens.map((token) => this.renderToken(token))}
`
: html`
No API tokens created yet. Create one to authenticate with the registry.
`}
`;
}
private renderCreateForm(): TemplateResult {
return html`
`;
}
private renderToken(token: ISgToken): TemplateResult {
const isExpired = token.expiresAt && new Date(token.expiresAt) < new Date();
return html`
${token.name}
${token.tokenPrefix}...
${token.protocols.map(
(p) => html`${p}`
)}
Created ${this.formatDate(token.createdAt)}
${token.lastUsedAt ? html`Last used ${this.formatDate(token.lastUsedAt)}` : ''}
${token.usageCount} uses
${token.expiresAt
? isExpired
? html`Expired`
: html`Expires ${this.formatDate(token.expiresAt)}`
: html`No expiry`}
${token.organizationId ? html`Org-scoped` : ''}
`;
}
private toggleProtocol(proto: TSgProtocol) {
if (this.createProtocols.includes(proto)) {
this.createProtocols = this.createProtocols.filter((p) => p !== proto);
} else {
this.createProtocols = [...this.createProtocols, proto];
}
this.requestUpdate();
}
private handleCreate() {
if (!this.createName.trim()) return;
this.dispatchEvent(
new CustomEvent('create', {
detail: {
name: this.createName.trim(),
protocols: this.createProtocols,
scopes: [{ protocol: '*' as const, actions: ['read', 'write'] as const }],
organizationId: this.createOrgId || undefined,
expiresInDays: this.createExpiryDays || undefined,
},
bubbles: true,
composed: true,
})
);
this.showCreateForm = false;
this.createName = '';
this.createProtocols = [];
this.createOrgId = '';
this.createExpiryDays = 0;
this.requestUpdate();
}
private handleRevoke(tokenId: string) {
this.dispatchEvent(
new CustomEvent('revoke', {
detail: { tokenId },
bubbles: true,
composed: true,
})
);
}
private formatDate(dateStr: string): string {
if (!dateStr) return '';
try {
return new Date(dateStr).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' });
} catch {
return dateStr;
}
}
}