Files
catalog/ts_web/elements/sg-organizations-list-view.ts

444 lines
12 KiB
TypeScript

import {
DeesElement,
customElement,
html,
css,
cssManager,
property,
state,
type TemplateResult,
} from '@design.estate/dees-element';
import type { ISgOrganization } from '../interfaces.js';
declare global {
interface HTMLElementTagNameMap {
'sg-organizations-list-view': SgOrganizationsListView;
}
}
@customElement('sg-organizations-list-view')
export class SgOrganizationsListView extends DeesElement {
public static demo = () => html`
<div style="padding: 24px; max-width: 1200px; background: #09090b;">
<sg-organizations-list-view
.organizations=${[
{ id: 'org1', name: 'myorg', displayName: 'My Organization', description: 'Primary development organization', isPublic: true, memberCount: 12, createdAt: '2025-06-01' },
{ id: 'org2', name: 'acme-corp', displayName: 'ACME Corp', description: 'Enterprise packages and images', isPublic: false, memberCount: 48, createdAt: '2025-01-15' },
{ id: 'org3', name: 'oss-collective', displayName: 'Open Source Collective', isPublic: true, memberCount: 156, createdAt: '2024-11-20' },
]}
></sg-organizations-list-view>
</div>
`;
public static demoGroups = ['Organizations'];
@property({ type: Array })
public accessor organizations: ISgOrganization[] = [];
@state()
accessor showCreateForm: boolean = false;
@state()
accessor createName: string = '';
@state()
accessor createDisplayName: string = '';
@state()
accessor createDescription: string = '';
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-btn svg {
width: 14px;
height: 14px;
}
.orgs-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1px;
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
}
.org-card {
background: ${cssManager.bdTheme('#fff', '#111')};
padding: 20px;
cursor: pointer;
transition: background 100ms ease;
display: flex;
flex-direction: column;
gap: 8px;
}
.org-card:hover {
background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')};
}
.org-header {
display: flex;
align-items: center;
gap: 12px;
}
.org-avatar {
width: 40px;
height: 40px;
background: ${cssManager.bdTheme('#e5e5e5', '#333')};
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 700;
color: ${cssManager.bdTheme('#666', '#999')};
flex-shrink: 0;
}
.org-titles {
min-width: 0;
}
.org-name {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#111', '#fff')};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-handle {
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
color: ${cssManager.bdTheme('#888', '#777')};
}
.org-description {
font-size: 13px;
color: ${cssManager.bdTheme('#666', '#aaa')};
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.org-footer {
display: flex;
gap: 16px;
font-size: 12px;
color: ${cssManager.bdTheme('#888', '#777')};
margin-top: 4px;
}
.visibility-badge {
display: inline-flex;
align-items: center;
padding: 1px 6px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.visibility-badge.public {
background: rgba(34, 197, 94, 0.15);
color: #22c55e;
}
.visibility-badge.private {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
.empty-state {
text-align: center;
padding: 64px 32px;
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')};
background: ${cssManager.bdTheme('#fff', '#111')};
}
.empty-title {
font-size: 16px;
font-weight: 600;
color: ${cssManager.bdTheme('#111', '#fff')};
margin-bottom: 8px;
}
.empty-text {
font-size: 14px;
color: ${cssManager.bdTheme('#888', '#777')};
margin-bottom: 20px;
}
/* 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;
}
.create-form-title {
font-size: 16px;
font-weight: 600;
}
.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,
.form-textarea {
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;
max-width: 400px;
}
.form-input:focus,
.form-textarea:focus {
border-color: ${cssManager.bdTheme('#111', '#fff')};
}
.form-textarea {
resize: vertical;
min-height: 60px;
max-width: 400px;
}
.form-hint {
font-size: 12px;
color: ${cssManager.bdTheme('#aaa', '#666')};
}
.form-actions {
display: flex;
gap: 8px;
margin-top: 4px;
}
.submit-btn {
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;
}
.submit-btn:hover {
opacity: 0.85;
}
.cancel-btn {
padding: 8px 20px;
background: transparent;
border: 1px solid ${cssManager.bdTheme('#ddd', '#333')};
font-size: 13px;
font-weight: 500;
color: ${cssManager.bdTheme('#333', '#ddd')};
cursor: pointer;
transition: all 150ms ease;
}
.cancel-btn:hover {
border-color: ${cssManager.bdTheme('#111', '#fff')};
color: ${cssManager.bdTheme('#111', '#fff')};
}
`,
];
public render(): TemplateResult {
return html`
<div class="container">
<div class="header">
<div class="page-title">Organizations</div>
<button class="create-btn" @click=${this.handleCreate}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
New Organization
</button>
</div>
${this.showCreateForm
? html`
<div class="create-form">
<div class="create-form-title">Create Organization</div>
<div class="form-group">
<label class="form-label">Name *</label>
<input
type="text"
class="form-input"
.value=${this.createName}
@input=${(e: InputEvent) => { this.createName = (e.target as HTMLInputElement).value; }}
placeholder="my-org"
>
<span class="form-hint">Lowercase letters, numbers, and dashes only</span>
</div>
<div class="form-group">
<label class="form-label">Display Name</label>
<input
type="text"
class="form-input"
.value=${this.createDisplayName}
@input=${(e: InputEvent) => { this.createDisplayName = (e.target as HTMLInputElement).value; }}
placeholder="My Organization"
>
</div>
<div class="form-group">
<label class="form-label">Description</label>
<textarea
class="form-textarea"
.value=${this.createDescription}
@input=${(e: InputEvent) => { this.createDescription = (e.target as HTMLTextAreaElement).value; }}
placeholder="A short description of the organization"
></textarea>
</div>
<div class="form-actions">
<button class="submit-btn" @click=${this.handleCreateSubmit}>Create</button>
<button class="cancel-btn" @click=${this.handleCreateCancel}>Cancel</button>
</div>
</div>
`
: ''}
${this.organizations.length > 0
? html`
<div class="orgs-grid">
${this.organizations.map(
(org) => html`
<div class="org-card" @click=${() => this.handleSelect(org.id)}>
<div class="org-header">
<div class="org-avatar">${(org.displayName || org.name).charAt(0).toUpperCase()}</div>
<div class="org-titles">
<div class="org-name">${org.displayName || org.name}</div>
<div class="org-handle">@${org.name}</div>
</div>
</div>
${org.description ? html`<div class="org-description">${org.description}</div>` : ''}
<div class="org-footer">
<span>${org.memberCount} member${org.memberCount !== 1 ? 's' : ''}</span>
<span class="visibility-badge ${org.isPublic ? 'public' : 'private'}">
${org.isPublic ? 'Public' : 'Private'}
</span>
</div>
</div>
`
)}
</div>
`
: html`
<div class="empty-state">
<div class="empty-title">No organizations yet</div>
<div class="empty-text">Create an organization to start publishing packages.</div>
<button class="create-btn" @click=${this.handleCreate}>Create Organization</button>
</div>
`}
</div>
`;
}
private handleCreate() {
this.showCreateForm = true;
}
private handleCreateSubmit() {
if (!this.createName.trim()) return;
this.dispatchEvent(
new CustomEvent('create', {
detail: {
name: this.createName.trim(),
displayName: this.createDisplayName.trim(),
description: this.createDescription.trim(),
},
bubbles: true,
composed: true,
})
);
this.createName = '';
this.createDisplayName = '';
this.createDescription = '';
this.showCreateForm = false;
}
private handleCreateCancel() {
this.createName = '';
this.createDisplayName = '';
this.createDescription = '';
this.showCreateForm = false;
}
private handleSelect(organizationId: string) {
this.dispatchEvent(
new CustomEvent('select', {
detail: { organizationId },
bubbles: true,
composed: true,
})
);
}
}