feat(catalog): add ContractEditor and many editor subcomponents; implement SignPad and SignBox; update README and bump dependencies
This commit is contained in:
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
558
ts_web/elements/sdig-contract-header/sdig-contract-header.ts
Normal file
@@ -0,0 +1,558 @@
|
||||
/**
|
||||
* @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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user