559 lines
17 KiB
TypeScript
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 .icon=${'lucide:ChevronDown'} 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 .icon=${'lucide:Download'}></dees-icon>
|
|
</button>
|
|
<button class="action-btn" @click=${this.handleDuplicate} title="Duplicate">
|
|
<dees-icon .icon=${'lucide:Copy'}></dees-icon>
|
|
</button>
|
|
<button class="action-btn" @click=${this.handleShare} title="Share">
|
|
<dees-icon .icon=${'lucide:Share2'}></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>
|
|
`;
|
|
}
|
|
}
|