feat(catalog): add admin dashboard components

This commit is contained in:
2026-05-07 15:35:37 +00:00
parent 5dbbe90b43
commit 3992adbafa
17 changed files with 2832 additions and 802 deletions
+407
View File
@@ -0,0 +1,407 @@
import { DeesElement, html, property, customElement, css, type TemplateResult } from '@design.estate/dees-element';
import { idpElementStyles } from './tokens.js';
import './idp-badge.js';
export type TIdpDataTableCell = TemplateResult | string | number | null | undefined;
export interface IIdpDataTableColumn {
label: string;
width?: string;
align?: 'left' | 'center' | 'right';
mono?: boolean;
hideBelow?: 'mobile' | 'tablet';
}
export interface IIdpDataTableRow {
id?: string;
cells: TIdpDataTableCell[];
}
export interface IIdpDataTableTab {
id: string;
label: string;
count?: string | number;
}
export interface IIdpDataTableTabSelectEventDetail {
tabId: string;
}
declare global {
interface HTMLElementTagNameMap {
'idp-data-table': IdpDataTable;
}
}
@customElement('idp-data-table')
export class IdpDataTable extends DeesElement {
public static demo = () => html`
<idp-data-table
title="Recent activity"
badge="3 total"
.columns=${[
{ label: 'User' },
{ label: 'Action' },
{ label: 'Device', mono: true, hideBelow: 'mobile' },
{ label: 'Status' },
{ label: 'When', align: 'right', mono: true },
]}
.tabs=${[
{ id: 'all', label: 'All' },
{ id: 'pending', label: 'Pending' },
{ id: 'denied', label: 'Denied' },
]}
selected-tab="all"
.rows=${[
{
cells: [
html`<div class="identity-cell"><span class="identity-avatar">EU</span><div><div class="identity-primary">Example User</div><div class="identity-secondary">user@example.com</div></div></div>`,
'OAuth grant',
'Desktop',
html`<idp-badge variant="ok">approved</idp-badge>`,
'2m ago',
],
},
]}
></idp-data-table>
`;
public static demoGroups = ['idp.global v3 primitives'];
@property({ type: String })
public accessor title = '';
@property({ type: String })
public accessor subtitle = '';
@property({ type: String })
public accessor badge = '';
@property({ type: String, attribute: 'selected-tab' })
public accessor selectedTab = '';
@property({ type: String, attribute: 'empty-title' })
public accessor emptyTitle = 'No rows';
@property({ type: String, attribute: 'empty-description' })
public accessor emptyDescription = 'There is no data to display yet.';
@property({ type: Array })
public accessor columns: IIdpDataTableColumn[] = [];
@property({ type: Array })
public accessor rows: IIdpDataTableRow[] = [];
@property({ type: Array })
public accessor tabs: IIdpDataTableTab[] = [];
public static styles = [
...idpElementStyles,
css`
:host {
display: block;
}
.table-card {
overflow: hidden;
border: 1px solid var(--idp-border);
border-radius: 8px;
background: var(--idp-card);
}
.table-head {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--idp-border-soft);
}
.title-wrap {
min-width: 0;
}
.title-row {
display: flex;
align-items: center;
gap: 10px;
}
.title {
color: var(--idp-fg);
font-size: 13px;
font-weight: 600;
}
.subtitle {
margin-top: 2px;
color: var(--idp-muted-fg);
font-size: 11.5px;
}
.count-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
border: 1px solid var(--idp-border);
border-radius: 999px;
background: var(--idp-muted);
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.04em;
line-height: 16px;
}
.tabs {
display: flex;
align-items: center;
gap: 2px;
margin-left: auto;
}
.tab {
padding: 4px 10px;
border: 0;
border-radius: 4px;
background: transparent;
color: var(--idp-muted-fg);
cursor: pointer;
font-family: var(--idp-font);
font-size: 11px;
}
.tab:hover,
.tab.active {
background: var(--idp-bg-2);
color: var(--idp-fg);
}
.table-scroll {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border-bottom: 1px solid var(--idp-border-soft);
text-align: left;
vertical-align: middle;
}
th {
padding: 9px 16px;
background: var(--idp-muted);
color: var(--idp-fg-3);
font-family: var(--idp-mono);
font-size: 10px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
white-space: nowrap;
}
td {
padding: 10px 16px;
color: var(--idp-fg-2);
font-size: 12.5px;
}
tbody tr:last-child td {
border-bottom: 0;
}
tbody tr:hover td {
background: var(--idp-bg-2);
}
.align-center {
text-align: center;
}
.align-right {
text-align: right;
}
.mono-cell {
color: var(--idp-muted-fg);
font-family: var(--idp-mono);
font-size: 11.5px;
}
.identity-cell {
display: flex;
align-items: center;
gap: 8px;
min-width: 190px;
}
.identity-avatar {
width: 22px;
height: 22px;
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border: 1px solid var(--idp-border);
border-radius: 999px;
background: var(--idp-bg-2);
color: var(--avatar-color, var(--idp-accent));
font-family: var(--idp-mono);
font-size: 9.5px;
font-weight: 600;
}
.identity-primary {
color: var(--idp-fg);
font-size: 12.5px;
font-weight: 500;
}
.identity-secondary,
.cell-secondary {
margin-top: 1px;
color: var(--idp-muted-fg);
font-size: 11px;
line-height: 1.35;
}
.chip-row,
.cell-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.cell-actions {
justify-content: flex-end;
}
.table-action {
min-height: 28px;
padding: 5px 10px;
border: 1px solid var(--idp-border);
border-radius: 7px;
background: transparent;
color: var(--idp-fg);
cursor: pointer;
font-family: var(--idp-font);
font-size: 12px;
}
.table-action:hover {
background: var(--idp-bg-2);
}
.table-action.destructive {
border-color: var(--idp-error-border);
color: var(--idp-error);
}
.table-action.primary {
border-color: var(--idp-accent);
background: var(--idp-accent);
color: var(--idp-accent-fg);
}
.empty-state {
padding: 32px 20px;
color: var(--idp-muted-fg);
text-align: center;
}
.empty-title {
color: var(--idp-fg);
font-size: 13px;
font-weight: 600;
}
.empty-description {
max-width: 420px;
margin: 6px auto 0;
font-size: 12px;
line-height: 1.5;
}
@media (max-width: 900px) {
.hide-tablet {
display: none;
}
}
@media (max-width: 720px) {
.table-head {
align-items: flex-start;
flex-direction: column;
gap: 12px;
}
.tabs {
width: 100%;
margin-left: 0;
overflow-x: auto;
}
th,
td {
padding-left: 16px;
padding-right: 16px;
}
.hide-mobile {
display: none;
}
}
`,
];
private selectTab(tabIdArg: string) {
this.selectedTab = tabIdArg;
this.dispatchEvent(new CustomEvent<IIdpDataTableTabSelectEventDetail>('idp-data-table-tab-select', {
detail: { tabId: tabIdArg },
bubbles: true,
composed: true,
}));
}
private columnClass(columnArg: IIdpDataTableColumn): string {
const classes = [
columnArg.align === 'center' ? 'align-center' : '',
columnArg.align === 'right' ? 'align-right' : '',
columnArg.hideBelow === 'mobile' ? 'hide-mobile' : '',
columnArg.hideBelow === 'tablet' ? 'hide-tablet' : '',
];
return classes.filter(Boolean).join(' ');
}
private renderCell(contentArg: TIdpDataTableCell, columnArg: IIdpDataTableColumn): TemplateResult {
const content = contentArg === null || contentArg === undefined || contentArg === '' ? '-' : contentArg;
return columnArg.mono ? html`<span class="mono-cell">${content}</span>` : html`${content}`;
}
public render(): TemplateResult {
const selectedTab = this.selectedTab || this.tabs[0]?.id || '';
return html`
<section class="table-card">
${this.title || this.badge || this.tabs.length ? html`
<div class="table-head">
<div class="title-wrap">
<div class="title-row">
${this.title ? html`<div class="title">${this.title}</div>` : html``}
${this.badge ? html`<span class="count-pill">${this.badge}</span>` : html``}
</div>
${this.subtitle ? html`<div class="subtitle">${this.subtitle}</div>` : html``}
</div>
${this.tabs.length ? html`
<div class="tabs" role="tablist">
${this.tabs.map((tabArg) => html`
<button
class="tab ${tabArg.id === selectedTab ? 'active' : ''}"
role="tab"
aria-selected=${tabArg.id === selectedTab ? 'true' : 'false'}
@click=${() => this.selectTab(tabArg.id)}
>${tabArg.label}${tabArg.count !== undefined ? html` (${tabArg.count})` : html``}</button>
`)}
</div>
` : html``}
</div>
` : html``}
${this.rows.length ? html`
<div class="table-scroll">
<table>
<thead>
<tr>
${this.columns.map((columnArg) => html`
<th class=${this.columnClass(columnArg)} style=${columnArg.width ? `width:${columnArg.width}` : ''}>${columnArg.label}</th>
`)}
</tr>
</thead>
<tbody>
${this.rows.map((rowArg) => html`
<tr>
${this.columns.map((columnArg, indexArg) => html`
<td class=${this.columnClass(columnArg)}>${this.renderCell(rowArg.cells[indexArg], columnArg)}</td>
`)}
</tr>
`)}
</tbody>
</table>
</div>
` : html`
<div class="empty-state">
<div class="empty-title">${this.emptyTitle}</div>
<div class="empty-description">${this.emptyDescription}</div>
</div>
`}
</section>
`;
}
}