/** * @file sdig-contracteditor.ts * @description Main contract editor orchestrator component */ import { DeesElement, property, state, html, customElement, type TemplateResult, css, cssManager, } from '@design.estate/dees-element'; import * as plugins from '../../plugins.js'; import { createEditorStore, type TEditorStore } from './state.js'; import { type TEditorSection, type IEditorState, EDITOR_SECTIONS, type IContractChangeEventDetail, type ISectionChangeEventDetail, } from './types.js'; // Import sub-components import '../sdig-contract-header/sdig-contract-header.js'; import '../sdig-contract-metadata/sdig-contract-metadata.js'; import '../sdig-contract-parties/sdig-contract-parties.js'; import '../sdig-contract-content/sdig-contract-content.js'; import '../sdig-contract-terms/sdig-contract-terms.js'; import '../sdig-contract-signatures/sdig-contract-signatures.js'; import '../sdig-contract-attachments/sdig-contract-attachments.js'; import '../sdig-contract-collaboration/sdig-contract-collaboration.js'; import '../sdig-contract-audit/sdig-contract-audit.js'; declare global { interface HTMLElementTagNameMap { 'sdig-contracteditor': SdigContracteditor; } } @customElement('sdig-contracteditor') export class SdigContracteditor extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; width: 100%; height: 100%; min-height: 600px; } .editor-container { display: flex; flex-direction: column; height: 100%; background: ${cssManager.bdTheme('#f8f9fa', '#09090b')}; border-radius: 8px; overflow: hidden; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } /* Header */ .editor-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 24px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .header-left { display: flex; align-items: center; gap: 16px; } .contract-title { font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; margin: 0; } /* shadcn-style badge */ .contract-status { display: inline-flex; align-items: center; padding: 2px 10px; border-radius: 6px; font-size: 12px; font-weight: 500; line-height: 1.4; border: 1px solid transparent; 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%)')}; } .contract-status.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%)')}; } .contract-status.executed { 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%)')}; } .header-right { display: flex; align-items: center; gap: 12px; } .dirty-indicator { display: flex; align-items: center; gap: 6px; font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .dirty-dot { width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; } .collaborators { display: flex; align-items: center; gap: -8px; } .collaborator-avatar { width: 32px; height: 32px; border-radius: 50%; border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 600; color: white; margin-left: -8px; } .collaborator-avatar:first-child { margin-left: 0; } /* Navigation Tabs */ .editor-nav { display: flex; align-items: center; gap: 4px; padding: 0 24px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; overflow-x: auto; } .nav-tab { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; background: transparent; border: none; border-bottom: 2px solid transparent; cursor: pointer; transition: all 0.15s ease; white-space: nowrap; } .nav-tab:hover { color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#f3f4f6', '#18181b')}; } .nav-tab.active { color: ${cssManager.bdTheme('#111111', '#fafafa')}; border-bottom-color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .nav-tab dees-icon { font-size: 16px; } .nav-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 20px; height: 20px; padding: 0 6px; border-radius: 10px; font-size: 11px; font-weight: 600; background: ${cssManager.bdTheme('#ef4444', '#dc2626')}; color: white; } /* Main Content Area */ .editor-main { display: flex; flex: 1; overflow: hidden; } .editor-content { flex: 1; overflow-y: auto; padding: 24px; } .editor-sidebar { width: 320px; border-left: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; overflow-y: auto; } /* Section placeholder */ .section-placeholder { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 400px; text-align: center; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .section-placeholder dees-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; } .section-placeholder h3 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .section-placeholder p { margin: 0; font-size: 14px; } /* Footer */ .editor-footer { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border-top: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .footer-left { display: flex; align-items: center; gap: 16px; font-size: 13px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .footer-right { display: flex; align-items: center; gap: 12px; } /* Buttons */ .btn { display: inline-flex; align-items: center; gap: 8px; padding: 8px 16px; font-size: 14px; font-weight: 500; border-radius: 6px; border: none; cursor: pointer; transition: all 0.15s ease; } .btn-primary { background: ${cssManager.bdTheme('#111111', '#fafafa')}; color: ${cssManager.bdTheme('#ffffff', '#09090b')}; } .btn-primary:hover { background: ${cssManager.bdTheme('#333333', '#e5e5e5')}; } .btn-secondary { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .btn-secondary:hover { background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')}; } .btn-ghost { background: transparent; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } /* Loading state */ .loading-overlay { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; background: ${cssManager.bdTheme('rgba(255,255,255,0.8)', 'rgba(0,0,0,0.8)')}; z-index: 100; } /* Overview section layout */ .overview-section { display: flex; flex-direction: column; gap: 24px; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor showSidebar: boolean = true; @property({ type: String }) public accessor initialSection: TEditorSection = 'overview'; // ============================================================================ // STATE // ============================================================================ @state() private accessor editorState: IEditorState | null = null; // ============================================================================ // INSTANCE // ============================================================================ private store: TEditorStore | null = null; private unsubscribe: (() => void) | null = null; private storeReady: Promise; private resolveStoreReady!: () => void; constructor() { super(); this.storeReady = new Promise((resolve) => { this.resolveStoreReady = resolve; }); } // ============================================================================ // LIFECYCLE // ============================================================================ public connectedCallback() { super.connectedCallback(); this.initStore(); } private async initStore() { this.store = await createEditorStore(); this.unsubscribe = this.store.subscribe((state) => { this.editorState = state; }); // Set initial section this.store.setActiveSection(this.initialSection); this.resolveStoreReady(); // If contract was already set, apply it now if (this.contract) { this.store.setContract(this.contract); } } public disconnectedCallback() { super.disconnectedCallback(); if (this.unsubscribe) { this.unsubscribe(); } } public async updated(changedProperties: Map) { if (changedProperties.has('contract') && this.contract) { await this.storeReady; this.store?.setContract(this.contract); } } // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleSectionChange(section: TEditorSection) { const previousSection = this.editorState?.activeSection || 'overview'; this.store?.setActiveSection(section); this.dispatchEvent( new CustomEvent('section-change', { detail: { section, previousSection }, bubbles: true, composed: true, }) ); } private handleSave() { if (!this.editorState?.contract) return; this.store?.setSaving(true); this.dispatchEvent( new CustomEvent('contract-save', { detail: { contract: this.editorState.contract, isDraft: this.editorState.contract.lifecycle.currentStatus === 'draft', }, bubbles: true, composed: true, }) ); } private handleDiscard() { this.store?.discardChanges(); this.dispatchEvent( new CustomEvent('contract-discard', { bubbles: true, composed: true, }) ); } private handleUndo() { this.store?.undo(); } private handleRedo() { this.store?.redo(); } // ============================================================================ // PUBLIC API // ============================================================================ /** * Update a field in the contract */ public updateField(path: string, value: unknown, description?: string) { this.store?.updateContract(path, value, description); this.dispatchEvent( new CustomEvent('contract-change', { detail: { path, value, source: 'user' }, bubbles: true, composed: true, }) ); } /** * Get current contract state */ public getContract(): plugins.sdInterfaces.IPortableContract | null { return this.editorState?.contract || null; } /** * Mark contract as saved externally */ public markSaved() { this.store?.markSaved(); } // ============================================================================ // RENDER HELPERS // ============================================================================ private getStatusClass(status: string): string { if (status === 'draft' || status === 'internal_review') return 'draft'; if (status === 'executed' || status === 'active') return 'executed'; return ''; } private formatStatus(status: string): string { return status.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); } private handleFieldChange(e: CustomEvent<{ path: string; value: unknown }>) { const { path, value } = e.detail; this.updateField(path, value); } private renderSectionContent(): TemplateResult { const section = this.editorState?.activeSection || 'overview'; const contract = this.editorState?.contract; const sectionConfig = EDITOR_SECTIONS.find((s) => s.id === section); // Render section based on active tab switch (section) { case 'overview': return this.renderOverviewSection(); case 'parties': return this.renderPartiesSection(); case 'content': return this.renderContentSection(); case 'terms': return this.renderTermsSection(); case 'signatures': return this.renderSignaturesSection(); case 'attachments': return this.renderAttachmentsSection(); case 'collaboration': return this.renderCollaborationSection(); case 'audit': return this.renderAuditSection(); default: return this.renderPlaceholder(sectionConfig, 'This section is being implemented...'); } } private renderOverviewSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderPartiesSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderContentSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderTermsSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderSignaturesSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderAttachmentsSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderCollaborationSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderAuditSection(): TemplateResult { const contract = this.editorState?.contract; if (!contract) { return html`No contract loaded`; } return html` `; } private renderPlaceholder(sectionConfig: typeof EDITOR_SECTIONS[0] | undefined, message: string): TemplateResult { return html` ${sectionConfig?.label || 'Section'} ${message} `; } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { const contract = this.editorState?.contract; const activeSection = this.editorState?.activeSection || 'overview'; const isDirty = this.editorState?.isDirty || false; const isSaving = this.editorState?.isSaving || false; const collaborators = this.editorState?.activeCollaborators || []; return html` ${contract?.title || 'Untitled Contract'} ${contract?.lifecycle?.currentStatus ? html` ${this.formatStatus(contract.lifecycle.currentStatus)} ` : ''} ${isDirty ? html` Unsaved changes ` : ''} ${collaborators.length > 0 ? html` ${collaborators.slice(0, 3).map( (c) => html` ${c.displayName.charAt(0).toUpperCase()} ` )} ${collaborators.length > 3 ? html` +${collaborators.length - 3} ` : ''} ` : ''} ${EDITOR_SECTIONS.map( (section) => html` this.handleSectionChange(section.id)} ?disabled=${section.disabled} > ${section.label} ${section.badge ? html`${section.badge}` : ''} ` )} ${this.renderSectionContent()} ${this.showSidebar ? html` ` : ''} ${this.editorState?.isLoading ? html` ` : ''} `; } }
${message}