feat(catalog): add admin dashboard components
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user