import { DeesElement, customElement, html, css, cssManager, property, state, type TemplateResult, } from '@design.estate/dees-element'; import type { ISgOrganizationDetail, ISgRepository, ISgOrganizationMember, ISgOrgRedirect, TSgOrgRole, } from '../interfaces.js'; import './sg-protocol-badge.js'; declare global { interface HTMLElementTagNameMap { 'sg-organization-detail-view': SgOrganizationDetailView; } } @customElement('sg-organization-detail-view') export class SgOrganizationDetailView extends DeesElement { public static demo = () => html`
`; public static demoGroups = ['Organizations']; @property({ type: Object }) public accessor organization: ISgOrganizationDetail = { id: '', name: '', displayName: '', isPublic: true, memberCount: 0, createdAt: '', usedStorageBytes: 0, storageQuotaBytes: 0, }; @property({ type: Array }) public accessor repositories: ISgRepository[] = []; @property({ type: Array }) public accessor members: ISgOrganizationMember[] = []; @property({ type: Array }) public accessor redirects: ISgOrgRedirect[] = []; @state() accessor editing: boolean = false; @state() accessor editName: string = ''; @state() accessor editDisplayName: string = ''; @state() accessor editDescription: string = ''; @state() accessor editWebsite: string = ''; @state() accessor editIsPublic: boolean = false; @state() accessor showDeleteConfirm: boolean = false; @state() accessor deleteConfirmName: string = ''; public static styles = [ cssManager.defaultStyles, css` :host { display: block; color: ${cssManager.bdTheme('#111', '#fff')}; } .container { display: flex; flex-direction: column; gap: 32px; } /* Back button */ .back-btn { display: inline-flex; align-items: center; gap: 6px; padding: 0; background: transparent; border: none; font-size: 13px; color: ${cssManager.bdTheme('#666', '#999')}; cursor: pointer; transition: color 150ms ease; } .back-btn:hover { color: ${cssManager.bdTheme('#111', '#fff')}; } /* Org header */ .org-header { display: flex; flex-direction: column; gap: 12px; } .org-top { display: flex; align-items: center; gap: 16px; } .org-avatar { width: 56px; height: 56px; background: ${cssManager.bdTheme('#e5e5e5', '#333')}; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: 700; color: ${cssManager.bdTheme('#666', '#999')}; flex-shrink: 0; } .org-titles h1 { font-size: 24px; font-weight: 700; margin: 0; letter-spacing: -0.02em; } .org-handle { font-size: 14px; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#888', '#777')}; } .org-description { font-size: 14px; color: ${cssManager.bdTheme('#666', '#aaa')}; line-height: 1.5; } .org-meta-bar { display: flex; gap: 16px; font-size: 13px; color: ${cssManager.bdTheme('#888', '#777')}; flex-wrap: wrap; } .org-meta-bar span { display: inline-flex; align-items: center; gap: 4px; } .storage-bar { width: 120px; height: 4px; background: ${cssManager.bdTheme('#e5e5e5', '#333')}; display: inline-block; vertical-align: middle; } .storage-fill { height: 100%; background: ${cssManager.bdTheme('#111', '#fff')}; } /* Section */ .section { display: flex; flex-direction: column; gap: 12px; } .section-header { display: flex; justify-content: space-between; align-items: center; } .section-title { font-size: 16px; font-weight: 600; } .add-btn { display: inline-flex; align-items: center; gap: 6px; padding: 6px 14px; 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; } .add-btn:hover { background: ${cssManager.bdTheme('#111', '#fff')}; color: ${cssManager.bdTheme('#fff', '#111')}; border-color: ${cssManager.bdTheme('#111', '#fff')}; } /* Repo grid */ .repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1px; background: ${cssManager.bdTheme('#e5e5e5', '#333')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .repo-card { background: ${cssManager.bdTheme('#fff', '#111')}; padding: 16px; cursor: pointer; transition: background 100ms ease; display: flex; flex-direction: column; gap: 8px; } .repo-card:hover { background: ${cssManager.bdTheme('#fafafa', '#1a1a1a')}; } .repo-name { font-size: 15px; font-weight: 600; font-family: 'JetBrains Mono', monospace; display: flex; align-items: center; gap: 8px; } .repo-description { font-size: 13px; color: ${cssManager.bdTheme('#666', '#aaa')}; } .repo-meta { display: flex; gap: 12px; font-size: 12px; color: ${cssManager.bdTheme('#888', '#777')}; } .visibility-tag { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; padding: 1px 6px; } .visibility-tag.public { background: rgba(34, 197, 94, 0.15); color: #22c55e; } .visibility-tag.private { background: rgba(239, 68, 68, 0.15); color: #ef4444; } .visibility-tag.internal { background: rgba(59, 130, 246, 0.15); color: #3b82f6; } /* Members list */ .members-list { display: flex; flex-direction: column; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .member-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${cssManager.bdTheme('#fff', '#111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .member-row:last-child { border-bottom: none; } .member-info { display: flex; align-items: center; gap: 12px; } .member-avatar { width: 32px; height: 32px; background: ${cssManager.bdTheme('#e5e5e5', '#333')}; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#666', '#999')}; } .member-name { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#111', '#fff')}; } .member-username { font-size: 12px; font-family: 'JetBrains Mono', monospace; color: ${cssManager.bdTheme('#888', '#777')}; } .member-actions { display: flex; align-items: center; gap: 8px; } .role-badge { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; padding: 2px 8px; background: ${cssManager.bdTheme('#f5f5f5', '#1a1a1a')}; color: ${cssManager.bdTheme('#666', '#aaa')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; } .remove-btn { padding: 4px 10px; background: transparent; border: 1px solid rgba(239, 68, 68, 0.3); font-size: 12px; color: #ef4444; cursor: pointer; transition: all 150ms ease; } .remove-btn:hover { background: rgba(239, 68, 68, 0.15); } .empty-state { text-align: center; padding: 32px; font-size: 14px; color: ${cssManager.bdTheme('#888', '#777')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; background: ${cssManager.bdTheme('#fff', '#111')}; } /* Header actions */ .header-actions { display: flex; gap: 8px; margin-left: auto; } .edit-btn { padding: 6px 14px; 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; } .edit-btn:hover { background: ${cssManager.bdTheme('#111', '#fff')}; color: ${cssManager.bdTheme('#fff', '#111')}; border-color: ${cssManager.bdTheme('#111', '#fff')}; } .delete-btn { padding: 6px 14px; background: transparent; border: 1px solid rgba(239, 68, 68, 0.3); font-size: 13px; font-weight: 500; color: #ef4444; cursor: pointer; transition: all 150ms ease; } .delete-btn:hover { background: rgba(239, 68, 68, 0.15); } /* Edit form */ .edit-form { background: ${cssManager.bdTheme('#fff', '#111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#333')}; padding: 24px; display: flex; flex-direction: column; gap: 16px; } .edit-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; } .form-toggle { display: flex; align-items: center; gap: 8px; font-size: 14px; color: ${cssManager.bdTheme('#111', '#fff')}; cursor: pointer; } .form-toggle input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; } .form-actions { display: flex; gap: 8px; margin-top: 4px; } .save-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; } .save-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')}; } /* Danger zone */ .danger-zone { background: ${cssManager.bdTheme('#fff', '#111')}; border: 1px solid rgba(239, 68, 68, 0.3); padding: 24px; display: flex; flex-direction: column; gap: 16px; } .danger-zone-title { font-size: 16px; font-weight: 600; color: #ef4444; } .danger-zone-text { font-size: 13px; color: ${cssManager.bdTheme('#666', '#aaa')}; line-height: 1.5; } .danger-zone-warning { font-size: 13px; font-weight: 600; color: #ef4444; } .danger-confirm-btn { align-self: flex-start; padding: 8px 16px; background: #ef4444; border: none; font-size: 13px; font-weight: 600; color: #fff; cursor: pointer; transition: opacity 150ms ease; } .danger-confirm-btn:hover { opacity: 0.85; } .danger-confirm-btn:disabled { opacity: 0.4; cursor: not-allowed; } .danger-cancel-btn { align-self: flex-start; padding: 8px 16px; 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; } .danger-cancel-btn:hover { border-color: ${cssManager.bdTheme('#111', '#fff')}; color: ${cssManager.bdTheme('#111', '#fff')}; } `, ]; public render(): TemplateResult { const storagePercent = this.organization.storageQuotaBytes > 0 ? Math.round((this.organization.usedStorageBytes / this.organization.storageQuotaBytes) * 100) : 0; return html`
${(this.organization.displayName || this.organization.name).charAt(0).toUpperCase()}

${this.organization.displayName || this.organization.name}

@${this.organization.name}
${this.editing ? html`
Edit Organization
{ this.editName = (e.target as HTMLInputElement).value; }} placeholder="my-org" > ${this.editName !== this.organization.name ? html`Renaming will create a redirect from "@${this.organization.name}"` : html`Lowercase letters, numbers, and dashes`}
{ this.editDisplayName = (e.target as HTMLInputElement).value; }} placeholder="Organization display name" >
{ this.editWebsite = (e.target as HTMLInputElement).value; }} placeholder="https://..." >
` : html` ${this.organization.description ? html`
${this.organization.description}
` : ''}
${this.organization.memberCount} members ${this.organization.isPublic ? 'Public' : 'Private'} ${this.organization.website ? html`${this.organization.website}` : ''} Storage: ${this.formatBytes(this.organization.usedStorageBytes)} / ${this.formatBytes(this.organization.storageQuotaBytes)}
`}
Repositories (${this.repositories.length})
${this.repositories.length > 0 ? html`
${this.repositories.map( (repo) => html`
this.emitEvent('select-repo', { repositoryId: repo.id })}>
${repo.name}
${repo.description ? html`
${repo.description}
` : ''}
${repo.packageCount} packages ${repo.visibility}
` )}
` : html`
No repositories yet. Create one to start publishing packages.
`}
Members (${this.members.length})
${this.members.length > 0 ? html`
${this.members.map( (member) => html`
${(member.user?.displayName || member.user?.username || '?').charAt(0).toUpperCase()}
${member.user?.displayName || 'Unknown'}
@${member.user?.username || member.userId}
${member.role} ${member.role !== 'owner' ? html`` : ''}
` )}
` : html`
No members
`}
${this.redirects.length > 0 ? html`
Handle Redirects (${this.redirects.length})
${this.redirects.map( (redirect) => html`
@${redirect.oldName}
redirects to @${this.organization.name}
` )}
` : ''} ${this.showDeleteConfirm ? html`
Delete Organization
This action is irreversible. All repositories, packages, and data belonging to @${this.organization.name} will be permanently deleted.
Type ${this.organization.name} to confirm deletion.
{ this.deleteConfirmName = (e.target as HTMLInputElement).value; }} placeholder=${this.organization.name} >
` : ''}
`; } private formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`; } private startEdit() { this.editName = this.organization.name || ''; this.editDisplayName = this.organization.displayName || ''; this.editDescription = this.organization.description || ''; this.editWebsite = this.organization.website || ''; this.editIsPublic = this.organization.isPublic; this.editing = true; } private handleEditSave() { const detail: Record = { organizationId: this.organization.id, displayName: this.editDisplayName.trim(), description: this.editDescription.trim(), website: this.editWebsite.trim(), isPublic: this.editIsPublic, }; // Only include name if it changed if (this.editName.trim() && this.editName.trim() !== this.organization.name) { detail.name = this.editName.trim(); } this.emitEvent('edit', detail); this.editing = false; } private handleEditCancel() { this.editing = false; } private handleDelete() { this.emitEvent('delete', { organizationId: this.organization.id, }); this.showDeleteConfirm = false; this.deleteConfirmName = ''; } private emitEvent(name: string, detail: Record) { this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true })); } }