/** * @file sdig-contract-collaboration.ts * @description Contract collaboration - comments, suggestions, and presence */ 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-collaboration': SdigContractCollaboration; } } // Comment interface interface IComment { id: string; userId: string; userName: string; userColor: string; content: string; createdAt: number; updatedAt?: number; anchorPath?: string; anchorText?: string; resolved: boolean; replies: IComment[]; } // Suggestion interface interface ISuggestion { id: string; userId: string; userName: string; userColor: string; originalText: string; suggestedText: string; path: string; status: 'pending' | 'accepted' | 'rejected'; createdAt: number; } // Presence interface interface IPresence { userId: string; userName: string; userColor: string; currentSection: string; cursorPosition?: { path: string; offset: number }; lastActive: number; } @customElement('sdig-contract-collaboration') export class SdigContractCollaboration extends DeesElement { // ============================================================================ // STATIC // ============================================================================ public static demo = () => html` `; public static styles = [ cssManager.defaultStyles, css` :host { display: block; } .collaboration-container { display: flex; flex-direction: column; gap: 24px; } /* Presence bar */ .presence-bar { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 12px; } .presence-info { display: flex; align-items: center; gap: 12px; } .presence-label { font-size: 14px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .presence-avatars { display: flex; align-items: center; } .presence-avatar { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: 600; color: white; margin-left: -8px; border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; cursor: pointer; position: relative; } .presence-avatar:first-child { margin-left: 0; } .presence-avatar .status-dot { position: absolute; bottom: 0; right: 0; width: 10px; height: 10px; border-radius: 50%; background: #10b981; border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; } .presence-avatar .status-dot.away { background: #f59e0b; } .presence-count { display: flex; align-items: center; justify-content: center; min-width: 36px; height: 36px; border-radius: 50%; background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; font-size: 13px; font-weight: 600; margin-left: -8px; border: 2px solid ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; } .share-btn { display: flex; align-items: center; gap: 8px; padding: 10px 16px; background: ${cssManager.bdTheme('#111111', '#fafafa')}; color: ${cssManager.bdTheme('#ffffff', '#09090b')}; border: none; border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; } .share-btn:hover { background: ${cssManager.bdTheme('#333333', '#e5e5e5')}; } /* 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')}; } .section-badge { padding: 2px 8px; border-radius: 9999px; font-size: 12px; font-weight: 600; background: ${cssManager.bdTheme('#dbeafe', '#1e3a5f')}; color: ${cssManager.bdTheme('#1e40af', '#93c5fd')}; } .section-content { padding: 20px; } /* Comments list */ .comments-list { display: flex; flex-direction: column; gap: 16px; } .comment-thread { padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .comment-thread.resolved { opacity: 0.6; } .comment-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; } .comment-avatar { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 13px; font-weight: 600; color: white; flex-shrink: 0; } .comment-meta { flex: 1; } .comment-author { font-size: 14px; font-weight: 600; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .comment-time { font-size: 12px; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .comment-anchor { display: inline-flex; align-items: center; gap: 4px; padding: 4px 8px; background: ${cssManager.bdTheme('#fef3c7', '#422006')}; border-radius: 4px; font-size: 12px; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; margin-bottom: 10px; cursor: pointer; } .comment-anchor:hover { background: ${cssManager.bdTheme('#fde68a', '#713f12')}; } .comment-content { font-size: 14px; line-height: 1.6; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; margin-bottom: 12px; } .comment-actions { display: flex; align-items: center; gap: 8px; } .comment-replies { margin-top: 16px; padding-left: 16px; border-left: 2px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; } .reply-item { padding: 12px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border-radius: 8px; margin-bottom: 8px; } .reply-item:last-child { margin-bottom: 0; } /* Suggestions list */ .suggestions-list { display: flex; flex-direction: column; gap: 12px; } .suggestion-card { padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .suggestion-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; } .suggestion-user { display: flex; align-items: center; gap: 8px; } .suggestion-status { display: flex; align-items: center; gap: 4px; padding: 4px 10px; border-radius: 9999px; font-size: 12px; font-weight: 500; } .suggestion-status.pending { background: ${cssManager.bdTheme('#fef3c7', '#422006')}; color: ${cssManager.bdTheme('#92400e', '#fcd34d')}; } .suggestion-status.accepted { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#065f46', '#6ee7b7')}; } .suggestion-status.rejected { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#fca5a5')}; } .suggestion-diff { padding: 12px; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 8px; font-family: 'Roboto Mono', monospace; font-size: 13px; line-height: 1.5; } .diff-removed { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#991b1b', '#fca5a5')}; text-decoration: line-through; padding: 2px 4px; border-radius: 2px; } .diff-added { background: ${cssManager.bdTheme('#d1fae5', '#064e3b')}; color: ${cssManager.bdTheme('#065f46', '#6ee7b7')}; padding: 2px 4px; border-radius: 2px; } .suggestion-actions { display: flex; gap: 8px; margin-top: 12px; } /* New comment input */ .new-comment { display: flex; gap: 12px; padding: 16px; background: ${cssManager.bdTheme('#f9fafb', '#111111')}; border: 1px solid ${cssManager.bdTheme('#e5e5e5', '#27272a')}; border-radius: 10px; } .new-comment-input { flex: 1; padding: 12px; font-size: 14px; color: ${cssManager.bdTheme('#111111', '#fafafa')}; background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')}; border: 1px solid ${cssManager.bdTheme('#d1d5db', '#3f3f46')}; border-radius: 8px; outline: none; resize: none; min-height: 80px; font-family: inherit; } .new-comment-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)')}; } .new-comment-input::placeholder { color: ${cssManager.bdTheme('#9ca3af', '#6b7280')}; } /* Empty state */ .empty-state { display: flex; flex-direction: column; align-items: center; padding: 40px 20px; text-align: center; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; } .empty-state dees-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; } .empty-state h4 { margin: 0 0 8px; font-size: 16px; font-weight: 600; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .empty-state p { margin: 0; font-size: 14px; } /* Filter tabs */ .filter-tabs { display: flex; gap: 4px; margin-bottom: 16px; } .filter-tab { padding: 8px 14px; font-size: 13px; font-weight: 500; color: ${cssManager.bdTheme('#6b7280', '#9ca3af')}; background: transparent; border: none; border-radius: 6px; cursor: pointer; transition: all 0.15s ease; } .filter-tab:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#374151', '#d1d5db')}; } .filter-tab.active { background: ${cssManager.bdTheme('#111111', '#fafafa')}; color: ${cssManager.bdTheme('#ffffff', '#09090b')}; } /* 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')}; } .btn-ghost:hover { background: ${cssManager.bdTheme('#f3f4f6', '#27272a')}; color: ${cssManager.bdTheme('#111111', '#fafafa')}; } .btn-success { background: ${cssManager.bdTheme('#10b981', '#059669')}; color: white; } .btn-success:hover { background: ${cssManager.bdTheme('#059669', '#047857')}; } .btn-danger { background: ${cssManager.bdTheme('#fee2e2', '#450a0a')}; color: ${cssManager.bdTheme('#dc2626', '#f87171')}; } .btn-danger:hover { background: ${cssManager.bdTheme('#fecaca', '#7f1d1d')}; } `, ]; // ============================================================================ // PROPERTIES // ============================================================================ @property({ type: Object }) public accessor contract: plugins.sdInterfaces.IPortableContract | null = null; @property({ type: Boolean }) public accessor readonly: boolean = false; // ============================================================================ // STATE // ============================================================================ @state() private accessor activeTab: 'comments' | 'suggestions' = 'comments'; @state() private accessor commentFilter: 'all' | 'open' | 'resolved' = 'all'; @state() private accessor newCommentText: string = ''; // Demo presence data @state() private accessor presenceList: IPresence[] = [ { userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', currentSection: 'content', lastActive: Date.now() }, { userId: '2', userName: 'Bob Johnson', userColor: '#10b981', currentSection: 'parties', lastActive: Date.now() - 60000 }, { userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', currentSection: 'terms', lastActive: Date.now() - 300000 }, ]; // Demo comments data @state() private accessor comments: IComment[] = [ { id: '1', userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', content: 'Can we clarify the payment terms in paragraph 3? The current wording seems ambiguous.', createdAt: Date.now() - 3600000, anchorPath: 'paragraphs.2', anchorText: 'Compensation', resolved: false, replies: [ { id: '1-1', userId: '2', userName: 'Bob Johnson', userColor: '#10b981', content: 'Good point. I\'ll update the wording to be more specific.', createdAt: Date.now() - 1800000, resolved: false, replies: [], }, ], }, { id: '2', userId: '3', userName: 'Carol Davis', userColor: '#f59e0b', content: 'The termination clause needs to comply with the latest regulations.', createdAt: Date.now() - 86400000, resolved: true, replies: [], }, ]; // Demo suggestions data @state() private accessor suggestions: ISuggestion[] = [ { id: '1', userId: '1', userName: 'Alice Smith', userColor: '#3b82f6', originalText: 'monthly salary', suggestedText: 'monthly gross salary', path: 'paragraphs.2.content', status: 'pending', createdAt: Date.now() - 7200000, }, ]; // ============================================================================ // EVENT HANDLERS // ============================================================================ private handleFieldChange(path: string, value: unknown) { this.dispatchEvent( new CustomEvent('field-change', { detail: { path, value }, bubbles: true, composed: true, }) ); } private handleAddComment() { if (!this.newCommentText.trim()) return; const newComment: IComment = { id: `comment-${Date.now()}`, userId: 'current-user', userName: 'You', userColor: '#6366f1', content: this.newCommentText, createdAt: Date.now(), resolved: false, replies: [], }; this.comments = [newComment, ...this.comments]; this.newCommentText = ''; } private handleResolveComment(commentId: string) { this.comments = this.comments.map((c) => c.id === commentId ? { ...c, resolved: !c.resolved } : c ); } private handleAcceptSuggestion(suggestionId: string) { this.suggestions = this.suggestions.map((s) => s.id === suggestionId ? { ...s, status: 'accepted' as const } : s ); } private handleRejectSuggestion(suggestionId: string) { this.suggestions = this.suggestions.map((s) => s.id === suggestionId ? { ...s, status: 'rejected' as const } : s ); } // ============================================================================ // HELPERS // ============================================================================ private getFilteredComments(): IComment[] { if (this.commentFilter === 'all') return this.comments; if (this.commentFilter === 'open') return this.comments.filter((c) => !c.resolved); return this.comments.filter((c) => c.resolved); } private formatTimeAgo(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return 'just now'; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h ago`; const days = Math.floor(hours / 24); return `${days}d ago`; } private getActivePresence(): IPresence[] { const fiveMinutesAgo = Date.now() - 300000; return this.presenceList.filter((p) => p.lastActive > fiveMinutesAgo); } // ============================================================================ // RENDER // ============================================================================ public render(): TemplateResult { if (!this.contract) { return html`
No contract loaded
`; } const activePresence = this.getActivePresence(); const openComments = this.comments.filter((c) => !c.resolved).length; const pendingSuggestions = this.suggestions.filter((s) => s.status === 'pending').length; return html`
Currently viewing:
${activePresence.slice(0, 4).map( (p) => html`
${p.userName.charAt(0)}
` )} ${activePresence.length > 4 ? html`
+${activePresence.length - 4}
` : ''}
Comments ${openComments > 0 ? html`${openComments} open` : ''}
${!this.readonly ? html`
` : ''} ${this.getFilteredComments().length > 0 ? html`
${this.getFilteredComments().map((comment) => this.renderComment(comment))}
` : html`

No Comments

Start a discussion by adding a comment

`}
Suggestions ${pendingSuggestions > 0 ? html`${pendingSuggestions} pending` : ''}
${this.suggestions.length > 0 ? html`
${this.suggestions.map((suggestion) => this.renderSuggestion(suggestion))}
` : html`

No Suggestions

Suggested changes will appear here

`}
`; } private renderComment(comment: IComment): TemplateResult { return html`
${comment.userName.charAt(0)}
${comment.userName}
${this.formatTimeAgo(comment.createdAt)}
${!this.readonly ? html` ` : ''}
${comment.anchorText ? html`
${comment.anchorText}
` : ''}
${comment.content}
${comment.replies.length > 0 ? html`
${comment.replies.map( (reply) => html`
${reply.userName.charAt(0)}
${reply.userName}
${this.formatTimeAgo(reply.createdAt)}
${reply.content}
` )}
` : ''}
`; } private renderSuggestion(suggestion: ISuggestion): TemplateResult { return html`
${suggestion.userName.charAt(0)}
${suggestion.userName}
${this.formatTimeAgo(suggestion.createdAt)}
${suggestion.status.charAt(0).toUpperCase() + suggestion.status.slice(1)}
${suggestion.originalText} ${suggestion.suggestedText}
${suggestion.status === 'pending' && !this.readonly ? html`
` : ''}
`; } }