/** * @file sdig-contract-content.ts * @description Contract content/paragraphs editor component */ 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-content': SdigContractContent; } } // Paragraph type configuration const PARAGRAPH_TYPES = [ { value: 'section', label: 'Section', icon: 'lucide:Heading' }, { value: 'clause', label: 'Clause', icon: 'lucide:FileText' }, { value: 'definition', label: 'Definition', icon: 'lucide:BookOpen' }, { value: 'obligation', label: 'Obligation', icon: 'lucide:CheckSquare' }, { value: 'condition', label: 'Condition', icon: 'lucide:GitBranch' }, { value: 'schedule', label: 'Schedule', icon: 'lucide:Calendar' }, ]; @customElement('sdig-contract-content') export class SdigContractContent extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .content-container { display: flex; flex-direction: column; gap: 16px; } /* Toolbar */ .content-toolbar { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .toolbar-left { display: flex; align-items: center; gap: 12px; } .toolbar-right { display: flex; align-items: center; gap: 8px; } .search-box { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: ${cssManager.bdTheme('#f3f4f6', '#18181b')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 8px; } .search-box input { border: none; background: transparent; font-size: 14px; color: ${cssManager.bdTheme('#111111', '#fafafa')}; outline: none; width: 200px; } .search-box input::placeholder { color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; } .search-box dees-icon { font-size: 16px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } /* Section card */ .section-card { background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; overflow: hidden; } .section-header { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .section-title { display: flex; align-items: center; gap: 10px; font-size: 15px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .section-title dees-icon { font-size: 18px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .paragraph-count { font-size: 13px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .section-content { padding: 0; } /* Paragraph list */ .paragraphs-list { display: flex; flex-direction: column; } .paragraph-item { display: flex; align-items: flex-start; gap: 16px; padding: 20px; border-bottom: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; cursor: pointer; transition: background 0.15s ease; } .paragraph-item:last-child { border-bottom: none; } .paragraph-item:hover { background: ${cssManager.bdTheme('#f9fafb', '#111111')}; } .paragraph-item.selected { background: ${cssManager.bdTheme('#eff6ff', '#172554')}; border-color: ${cssManager.bdTheme('#bfdbfe', '#1e40af')}; } .paragraph-item.editing { background: ${cssManager.bdTheme('#f9fafb', '#111111')}; } .paragraph-drag-handle { display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; cursor: grab; flex-shrink: 0; margin-top: 2px; } .paragraph-drag-handle:hover { color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .paragraph-number { display: flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 8px; font-size: 14px; font-weight: 600; background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; flex-shrink: 0; } .paragraph-content { flex: 1; min-width: 0; } .paragraph-title-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; } .paragraph-title { font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .paragraph-title-input { flex: 1; font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 6px; padding: 8px 12px; outline: none; } .paragraph-title-input:focus { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')}; } .paragraph-type-badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 8px; border-radius: 4px; font-size: 11px; font-weight: 500; background: ${cssManager.bdTheme('#e5e7eb', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .paragraph-body { font-size: 14px; line-height: 1.6; color: ${cssManager.bdTheme('#4b5563', '#9ca3af')}; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; } .paragraph-body.expanded { -webkit-line-clamp: unset; overflow: visible; } .paragraph-body-textarea { width: 100%; min-height: 150px; font-size: 14px; line-height: 1.6; color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 6px; padding: 12px; outline: none; resize: vertical; font-family: inherit; } .paragraph-body-textarea:focus { border-color: ${cssManager.bdTheme('#3b82f6', '#60a5fa')}; box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(96, 165, 250, 0.1)')}; } .paragraph-meta { display: flex; align-items: center; gap: 16px; margin-top: 12px; font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .paragraph-meta-item { display: flex; align-items: center; gap: 4px; } .paragraph-actions { display: flex; flex-direction: column; gap: 4px; flex-shrink: 0; } .paragraph-edit-actions { display: flex; gap: 8px; margin-top: 16px; } /* Variable highlighting */ .variable { display: inline; padding: 2px 6px; border-radius: 4px; font-family: 'Roboto Mono', monospace; font-size: 13px; background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; } /* Child paragraphs */ .child-paragraphs { margin-left: 48px; border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; padding-left: 16px; } .child-paragraphs .paragraph-item { padding: 16px; } .child-paragraphs .paragraph-number { width: 28px; height: 28px; font-size: 12px; } /* Add paragraph button */ .add-paragraph-row { display: flex; align-items: center; justify-content: center; padding: 16px 20px; border-top: 1px dashed ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .add-paragraph-btn { display: flex; align-items: center; gap: 8px; padding: 10px 20px; background: transparent; border: 2px dashed ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 8px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; } .add-paragraph-btn:hover { border-color: ${cssManager.bdTheme('#9ca3af', '#52525b')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; background: ${cssManager.bdTheme('#f9fafb', '#18181b')}; } /* Empty state */ .empty-state { display: flex; flex-direction: column; align-items: center; padding: 60px 20px; text-align: center; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .empty-state dees-icon { font-size: 64px; margin-bottom: 20px; opacity: 0.5; } .empty-state h4 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .empty-state p { margin: 0 0 24px; font-size: 14px; max-width: 400px; } /* Buttons */ .btn { display: inline-flex; align-items: center; gap: 6px; padding: 8px 14px; font-size: 13px; font-weight: 500; border-radius: 6px; border: none; cursor: pointer; transition: all 0.15s ease; } .btn-sm { padding: 6px 10px; font-size: 12px; } .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')}; padding: 6px; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .btn-danger { background: ${cssManager.bdTheme('#fef2f2', '#450a0a')}; color: ${cssManager.bdTheme('#dc2626', '#fca5a5')}; } .btn-danger:hover { background: ${cssManager.bdTheme('#fee2e2', '#7f1d1d')}; } /* View mode toggle */ .view-toggle { display: flex; background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; border-radius: 6px; padding: 2px; } .view-toggle-btn { display: flex; align-items: center; justify-content: center; padding: 6px 10px; background: transparent; border: none; border-radius: 4px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; cursor: pointer; transition: all 0.15s ease; } .view-toggle-btn.active { background: ${cssManager.bdTheme('#ffffff', '#3f3f46')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); } .view-toggle-btn dees-icon { font-size: 16px; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor selectedParagraphId: string | null = null; @state() private accessor editingParagraphId: string | null = null; @state() private accessor searchQuery: string = ''; @state() private accessor viewMode: 'list' | 'outline' = 'list'; @state() private accessor expandedParagraphs: Set = new Set(); // Editing state @state() private accessor editTitle: string = ''; @state() private accessor editContent: string = ''; // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleSelectParagraph(paragraphId: string) { this.selectedParagraphId = this.selectedParagraphId === paragraphId ? null : paragraphId; this.dispatchEvent( new CustomEvent('paragraph-select', { detail: { paragraphId: this.selectedParagraphId }, bubbles: true, composed: true, }) ); } private handleEditParagraph(paragraph: plugins.sdInterfaces.IParagraph) { this.editingParagraphId = paragraph.uniqueId; this.editTitle = paragraph.title; this.editContent = paragraph.content; } private handleSaveEdit() { if (!this.contract || !this.editingParagraphId) return; const updatedParagraphs = this.contract.paragraphs.map((p) => { if (p.uniqueId === this.editingParagraphId) { return { ...p, title: this.editTitle, content: this.editContent }; } return p; }); this.handleFieldChange('paragraphs', updatedParagraphs); this.editingParagraphId = null; this.editTitle = ''; this.editContent = ''; } private handleCancelEdit() { this.editingParagraphId = null; this.editTitle = ''; this.editContent = ''; } private handleAddParagraph(parentId: string | null = null) { if (!this.contract) return; const newParagraph: plugins.sdInterfaces.IParagraph = { uniqueId: `p-${Date.now()}`, parent: parentId ? this.contract.paragraphs.find((p) => p.uniqueId === parentId) || null : null, title: 'New Paragraph', content: 'Enter paragraph content here...', }; const updatedParagraphs = [...this.contract.paragraphs, newParagraph]; this.handleFieldChange('paragraphs', updatedParagraphs); // Start editing the new paragraph this.handleEditParagraph(newParagraph); } private handleDeleteParagraph(paragraphId: string) { if (!this.contract) return; // Remove the paragraph and any children const idsToRemove = new Set([paragraphId]); // Find child paragraphs recursively const findChildren = (parentId: string) => { this.contract!.paragraphs.forEach((p) => { if (p.parent?.uniqueId === parentId) { idsToRemove.add(p.uniqueId); findChildren(p.uniqueId); } }); }; findChildren(paragraphId); const updatedParagraphs = this.contract.paragraphs.filter((p) => !idsToRemove.has(p.uniqueId)); this.handleFieldChange('paragraphs', updatedParagraphs); if (this.selectedParagraphId === paragraphId) { this.selectedParagraphId = null; } } private handleMoveParagraph(paragraphId: string, direction: 'up' | 'down') { if (!this.contract) return; const paragraphs = [...this.contract.paragraphs]; const index = paragraphs.findIndex((p) => p.uniqueId === paragraphId); if (index === -1) return; if (direction === 'up' && index === 0) return; if (direction === 'down' && index === paragraphs.length - 1) return; const newIndex = direction === 'up' ? index - 1 : index + 1; [paragraphs[index], paragraphs[newIndex]] = [paragraphs[newIndex], paragraphs[index]]; this.handleFieldChange('paragraphs', paragraphs); } private handleSearchChange(e: Event) { const input = e.target as HTMLInputElement; this.searchQuery = input.value; } private toggleExpanded(paragraphId: string) { const expanded = new Set(this.expandedParagraphs); if (expanded.has(paragraphId)) { expanded.delete(paragraphId); } else { expanded.add(paragraphId); } this.expandedParagraphs = expanded; } // ============================================================================ // HELPERS // ============================================================================ private getRootParagraphs(): plugins.sdInterfaces.IParagraph[] { if (!this.contract) return []; return this.contract.paragraphs.filter((p) => !p.parent); } private getChildParagraphs(parentId: string): plugins.sdInterfaces.IParagraph[] { if (!this.contract) return []; return this.contract.paragraphs.filter((p) => p.parent?.uniqueId === parentId); } private filterParagraphs(paragraphs: plugins.sdInterfaces.IParagraph[]): plugins.sdInterfaces.IParagraph[] { if (!this.searchQuery) return paragraphs; const query = this.searchQuery.toLowerCase(); return paragraphs.filter( (p) => p.title.toLowerCase().includes(query) || p.content.toLowerCase().includes(query) ); } private highlightVariables(content: string): TemplateResult { // Match {{variableName}} patterns const parts = content.split(/(\{\{[^}]+\}\})/g); return html`${parts.map((part) => part.startsWith('{{') && part.endsWith('}}') ? html`${part}` : part )}`; } private getParagraphNumber(paragraph: plugins.sdInterfaces.IParagraph, index: number): string { // Simple numbering - can be enhanced for hierarchical numbering return String(index + 1); } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const rootParagraphs = this.getRootParagraphs(); const filteredParagraphs = this.filterParagraphs(rootParagraphs); return html`
${!this.readonly ? html` ` : ''}
Contract Content
${this.contract.paragraphs.length} paragraphs
${filteredParagraphs.length > 0 ? html`
${filteredParagraphs.map((paragraph, index) => this.renderParagraph(paragraph, index) )}
${!this.readonly ? html`
` : ''} ` : html`

No Paragraphs Yet

Start building your contract by adding paragraphs. Each paragraph can contain clauses, definitions, or obligations.

${!this.readonly ? html` ` : ''}
`}
`; } private renderParagraph(paragraph: plugins.sdInterfaces.IParagraph, index: number): TemplateResult { const isSelected = this.selectedParagraphId === paragraph.uniqueId; const isEditing = this.editingParagraphId === paragraph.uniqueId; const isExpanded = this.expandedParagraphs.has(paragraph.uniqueId); const childParagraphs = this.getChildParagraphs(paragraph.uniqueId); return html`
!isEditing && this.handleSelectParagraph(paragraph.uniqueId)} > ${!this.readonly ? html`
` : ''}
${this.getParagraphNumber(paragraph, index)}
${isEditing ? html` (this.editTitle = (e.target as HTMLInputElement).value)} @click=${(e: Event) => e.stopPropagation()} placeholder="Paragraph title" />
` : html`
${paragraph.title}
${this.highlightVariables(paragraph.content)}
${paragraph.content.length > 200 ? html` ` : ''} `}
${!this.readonly && !isEditing ? html`
` : ''}
${childParagraphs.length > 0 ? html`
${childParagraphs.map((child, childIndex) => this.renderParagraph(child, childIndex))}
` : ''} `; } }