Files
catalog/ts_web/elements/sdig-contract-header/sdig-contract-header.ts

559 lines
17 KiB
TypeScript

/**
* @file sdig-contract-header.ts
* @description Contract header component with title, status, and quick actions
*/
import {
DeesElement,
property,
html,
customElement,
type TemplateResult,
css,
cssManager,
state,
} from '@design.estate/dees-element';
import * as plugins from '../../plugins.js';
declare global {
interface HTMLElementTagNameMap {
'sdig-contract-header': SdigContractHeader;
}
}
@customElement('sdig-contract-header')
export class SdigContractHeader extends DeesElement {
// ============================================================================
// STATIC
// ============================================================================
public static demo = () => html`
<sdig-contract-header
.contract=${plugins.sdDemodata.demoContract}
></sdig-contract-header>
`;
public static styles = [
cssManager.defaultStyles,
css`
:host {
display: block;
}
.header-card {
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
border-radius: 12px;
padding: 24px;
}
.header-top {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 20px;
}
.title-section {
flex: 1;
}
.contract-number {
font-size: 13px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
margin-bottom: 8px;
}
.title-input-wrapper {
position: relative;
}
.title-input {
width: 100%;
font-size: 24px;
font-weight: 700;
color: ${cssManager.bdTheme('#111111', '#fafafa')};
background: transparent;
border: none;
padding: 0;
outline: none;
border-bottom: 2px solid transparent;
transition: border-color 0.15s ease;
}
.title-input:focus {
border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')};
}
.title-input::placeholder {
color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
.status-section {
display: flex;
align-items: center;
gap: 12px;
}
/* shadcn-style badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 2px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
border: 1px solid transparent;
line-height: 1.4;
}
.status-badge:hover:not(:disabled) {
filter: brightness(0.95);
}
.status-badge:disabled {
cursor: default;
}
.status-badge.draft {
background: ${cssManager.bdTheme('hsl(48 96% 89%)', 'hsl(48 96% 15%)')};
color: ${cssManager.bdTheme('hsl(25 95% 30%)', 'hsl(48 96% 70%)')};
border-color: ${cssManager.bdTheme('hsl(48 96% 76%)', 'hsl(48 96% 25%)')};
}
.status-badge.review {
background: ${cssManager.bdTheme('hsl(214 95% 93%)', 'hsl(214 95% 15%)')};
color: ${cssManager.bdTheme('hsl(214 95% 35%)', 'hsl(214 95% 70%)')};
border-color: ${cssManager.bdTheme('hsl(214 95% 80%)', 'hsl(214 95% 25%)')};
}
.status-badge.pending {
background: ${cssManager.bdTheme('hsl(38 92% 90%)', 'hsl(38 92% 15%)')};
color: ${cssManager.bdTheme('hsl(25 95% 35%)', 'hsl(38 92% 65%)')};
border-color: ${cssManager.bdTheme('hsl(38 92% 75%)', 'hsl(38 92% 25%)')};
}
.status-badge.signed,
.status-badge.active {
background: ${cssManager.bdTheme('hsl(142 76% 90%)', 'hsl(142 76% 15%)')};
color: ${cssManager.bdTheme('hsl(142 76% 28%)', 'hsl(142 76% 65%)')};
border-color: ${cssManager.bdTheme('hsl(142 76% 75%)', 'hsl(142 76% 25%)')};
}
.status-badge.terminated {
background: ${cssManager.bdTheme('hsl(0 84% 92%)', 'hsl(0 84% 15%)')};
color: ${cssManager.bdTheme('hsl(0 84% 35%)', 'hsl(0 84% 65%)')};
border-color: ${cssManager.bdTheme('hsl(0 84% 80%)', 'hsl(0 84% 25%)')};
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
opacity: 0.8;
}
/* Meta info row */
.meta-row {
display: flex;
flex-wrap: wrap;
gap: 24px;
padding-top: 20px;
border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
}
.meta-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.meta-label {
font-size: 12px;
font-weight: 500;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
text-transform: uppercase;
letter-spacing: 0.05em;
}
.meta-value {
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#111111', '#fafafa')};
}
.meta-value.clickable {
cursor: pointer;
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
}
.meta-value.clickable:hover {
text-decoration: underline;
}
/* Tags */
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.tag {
display: inline-flex;
align-items: center;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
}
/* Quick actions */
.quick-actions {
display: flex;
gap: 8px;
}
.action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 8px;
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#18181b')};
color: ${cssManager.bdTheme('#111111', '#fafafa')};
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
}
.action-btn dees-icon {
font-size: 16px;
}
/* Status dropdown */
.status-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: 8px;
min-width: 200px;
background: ${cssManager.bdTheme('#ffffff', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')};
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 100;
overflow: hidden;
}
.status-option {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
font-size: 14px;
color: ${cssManager.bdTheme('#374151', '#d1d5db')};
cursor: pointer;
transition: background 0.1s ease;
}
.status-option:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#27272a')};
}
.status-option.selected {
background: ${cssManager.bdTheme('#eff6ff', '#1e3a5f')};
color: ${cssManager.bdTheme('#1e40af', '#93c5fd')};
}
`,
];
// ============================================================================
// PROPERTIES
// ============================================================================
@property({ type: Object })
public accessor contract: plugins.sdInterfaces.IPortableContract | null = null;
@property({ type: Boolean })
public accessor readonly: boolean = false;
// ============================================================================
// STATE
// ============================================================================
@state()
private accessor showStatusDropdown: boolean = false;
@state()
private accessor editingTitle: boolean = false;
// ============================================================================
// STATUS CONFIGURATION
// ============================================================================
private statusOptions: Array<{
value: plugins.sdInterfaces.TContractStatus;
label: string;
category: string;
}> = [
{ value: 'draft', label: 'Draft', category: 'draft' },
{ value: 'internal_review', label: 'Internal Review', category: 'review' },
{ value: 'legal_review', label: 'Legal Review', category: 'review' },
{ value: 'negotiation', label: 'Negotiation', category: 'review' },
{ value: 'pending_approval', label: 'Pending Approval', category: 'pending' },
{ value: 'pending_signature', label: 'Pending Signature', category: 'pending' },
{ value: 'partially_signed', label: 'Partially Signed', category: 'pending' },
{ value: 'signed', label: 'Signed', category: 'signed' },
{ value: 'executed', label: 'Executed', category: 'signed' },
{ value: 'active', label: 'Active', category: 'active' },
{ value: 'expired', label: 'Expired', category: 'terminated' },
{ value: 'terminated', label: 'Terminated', category: 'terminated' },
{ value: 'cancelled', label: 'Cancelled', category: 'terminated' },
{ value: 'voided', label: 'Voided', category: 'terminated' },
];
// ============================================================================
// EVENT HANDLERS
// ============================================================================
private handleTitleChange(e: Event) {
const input = e.target as HTMLInputElement;
this.dispatchEvent(
new CustomEvent('field-change', {
detail: { path: 'title', value: input.value },
bubbles: true,
composed: true,
})
);
}
private handleStatusChange(status: plugins.sdInterfaces.TContractStatus) {
this.showStatusDropdown = false;
this.dispatchEvent(
new CustomEvent('field-change', {
detail: { path: 'lifecycle.currentStatus', value: status },
bubbles: true,
composed: true,
})
);
}
private toggleStatusDropdown() {
if (!this.readonly) {
this.showStatusDropdown = !this.showStatusDropdown;
}
}
private handleExport() {
this.dispatchEvent(
new CustomEvent('action', {
detail: { action: 'export' },
bubbles: true,
composed: true,
})
);
}
private handleDuplicate() {
this.dispatchEvent(
new CustomEvent('action', {
detail: { action: 'duplicate' },
bubbles: true,
composed: true,
})
);
}
private handleShare() {
this.dispatchEvent(
new CustomEvent('action', {
detail: { action: 'share' },
bubbles: true,
composed: true,
})
);
}
// ============================================================================
// HELPERS
// ============================================================================
private getStatusCategory(status: string): string {
const option = this.statusOptions.find((o) => o.value === status);
return option?.category || 'draft';
}
private formatStatus(status: string): string {
const option = this.statusOptions.find((o) => o.value === status);
return option?.label || status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
private formatDate(timestamp: number): string {
return new Date(timestamp).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
}
private formatContractType(type: string): string {
return type.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
}
// ============================================================================
// RENDER
// ============================================================================
public render(): TemplateResult {
if (!this.contract) {
return html`<div class="header-card">No contract loaded</div>`;
}
const status = this.contract.lifecycle?.currentStatus || 'draft';
const statusCategory = this.getStatusCategory(status);
return html`
<div class="header-card">
<div class="header-top">
<div class="title-section">
${this.contract.metadata?.contractNumber
? html`<div class="contract-number">#${this.contract.metadata.contractNumber}</div>`
: ''}
<div class="title-input-wrapper">
<input
type="text"
class="title-input"
.value=${this.contract.title}
placeholder="Contract Title"
?disabled=${this.readonly}
@input=${this.handleTitleChange}
/>
</div>
</div>
<div class="status-section">
<div style="position: relative;">
<button
class="status-badge ${statusCategory}"
@click=${this.toggleStatusDropdown}
?disabled=${this.readonly}
>
<span class="status-dot"></span>
${this.formatStatus(status)}
${!this.readonly
? html`<dees-icon .iconFA=${'lucide:chevron-down'} style="font-size: 14px;"></dees-icon>`
: ''}
</button>
${this.showStatusDropdown
? html`
<div class="status-dropdown">
${this.statusOptions.map(
(option) => html`
<div
class="status-option ${status === option.value ? 'selected' : ''}"
@click=${() => this.handleStatusChange(option.value)}
>
<span
class="status-dot"
style="background: ${option.category === 'draft'
? '#f59e0b'
: option.category === 'review'
? '#3b82f6'
: option.category === 'pending'
? '#f59e0b'
: option.category === 'signed' || option.category === 'active'
? '#10b981'
: '#ef4444'}"
></span>
${option.label}
</div>
`
)}
</div>
`
: ''}
</div>
<div class="quick-actions">
<button class="action-btn" @click=${this.handleExport} title="Export">
<dees-icon .iconFA=${'lucide:download'}></dees-icon>
</button>
<button class="action-btn" @click=${this.handleDuplicate} title="Duplicate">
<dees-icon .iconFA=${'lucide:copy'}></dees-icon>
</button>
<button class="action-btn" @click=${this.handleShare} title="Share">
<dees-icon .iconFA=${'lucide:share-2'}></dees-icon>
</button>
</div>
</div>
</div>
${this.contract.metadata ? html`
<div class="meta-row">
<div class="meta-item">
<span class="meta-label">Type</span>
<span class="meta-value">${this.formatContractType(this.contract.metadata.contractType)}</span>
</div>
<div class="meta-item">
<span class="meta-label">Category</span>
<span class="meta-value">${this.formatContractType(this.contract.metadata.category)}</span>
</div>
<div class="meta-item">
<span class="meta-label">Language</span>
<span class="meta-value">${this.contract.metadata.language?.toUpperCase() || 'N/A'}</span>
</div>
<div class="meta-item">
<span class="meta-label">Jurisdiction</span>
<span class="meta-value">
${this.contract.metadata.governingLaw?.country || 'Not specified'}
${this.contract.metadata.governingLaw?.state
? `, ${this.contract.metadata.governingLaw.state}`
: ''}
</span>
</div>
<div class="meta-item">
<span class="meta-label">Created</span>
<span class="meta-value">${this.formatDate(this.contract.createdAt)}</span>
</div>
<div class="meta-item">
<span class="meta-label">Parties</span>
<span class="meta-value clickable">${this.contract.involvedParties?.length || 0} parties</span>
</div>
${this.contract.metadata.tags?.length > 0
? html`
<div class="meta-item">
<span class="meta-label">Tags</span>
<div class="tags-container">
${this.contract.metadata.tags.map((tag) => html`<span class="tag">${tag}</span>`)}
</div>
</div>
`
: ''}
</div>
` : ''}
</div>
`;
}
}