import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS, state, } from '@design.estate/dees-element'; // Import design tokens import { colors, bdTheme } from './00colors.js'; import { spacing, radius, shadows, transitions } from './00tokens.js'; import { fontFamilies, typography } from './00fonts.js'; // Types export interface IMessage { id: string; text: string; sender: 'user' | 'support'; time: string; status?: 'sending' | 'sent' | 'delivered' | 'read'; } export interface IConversationData { id: string; title: string; messages: IMessage[]; } declare global { interface HTMLElementTagNameMap { 'sio-conversation-view': SioConversationView; } } @customElement('sio-conversation-view') export class SioConversationView extends DeesElement { public static demo = () => html` `; @property({ type: Object }) public conversation: IConversationData | null = null; @state() private messageText: string = ''; @state() private isTyping: boolean = false; public static styles = [ cssManager.defaultStyles, css` :host { display: flex; flex-direction: column; height: 100%; background: ${bdTheme('background')}; font-family: ${unsafeCSS(fontFamilies.sans)}; } .header { padding: ${unsafeCSS(spacing[4])}; border-bottom: 1px solid ${bdTheme('border')}; background: ${bdTheme('card')}; display: flex; align-items: center; gap: ${unsafeCSS(spacing[3])}; } .back-button { display: none; } @media (max-width: 600px) { .back-button { display: block; } } .header-title { font-size: 1.125rem; line-height: 1.5; font-weight: 600; margin: 0; color: ${bdTheme('foreground')}; flex: 1; } .header-actions { display: flex; gap: ${unsafeCSS(spacing[2])}; } .messages-container { flex: 1; overflow-y: auto; padding: ${unsafeCSS(spacing[4])}; display: flex; flex-direction: column; gap: ${unsafeCSS(spacing[3])}; } .message { display: flex; align-items: flex-start; gap: ${unsafeCSS(spacing[3])}; max-width: 70%; } .message.user { align-self: flex-end; flex-direction: row-reverse; } .message-bubble { padding: ${unsafeCSS(spacing[3])}; border-radius: ${unsafeCSS(radius.lg)}; font-size: 0.875rem; line-height: 1.5; position: relative; } .message.support .message-bubble { background: ${bdTheme('muted')}; color: ${bdTheme('foreground')}; border-bottom-left-radius: ${unsafeCSS(spacing[1])}; } .message.user .message-bubble { background: ${bdTheme('primary')}; color: ${bdTheme('primaryForeground')}; border-bottom-right-radius: ${unsafeCSS(spacing[1])}; } .message-time { font-size: 0.75rem; line-height: 1.5; color: ${bdTheme('mutedForeground')}; margin-top: ${unsafeCSS(spacing[1])}; } .message.user .message-time { text-align: right; } .typing-indicator { display: flex; align-items: center; gap: ${unsafeCSS(spacing[1])}; padding: ${unsafeCSS(spacing[2])} ${unsafeCSS(spacing[3])}; background: ${bdTheme('muted')}; border-radius: ${unsafeCSS(radius.lg)}; width: fit-content; } .typing-dot { width: 8px; height: 8px; background: ${bdTheme('mutedForeground')}; border-radius: 50%; animation: typing 1.4s infinite; } .typing-dot:nth-child(2) { animation-delay: 0.2s; } .typing-dot:nth-child(3) { animation-delay: 0.4s; } @keyframes typing { 0%, 60%, 100% { opacity: 0.3; transform: scale(0.8); } 30% { opacity: 1; transform: scale(1); } } .input-container { padding: ${unsafeCSS(spacing[4])}; border-top: 1px solid ${bdTheme('border')}; background: ${bdTheme('card')}; } .input-wrapper { display: flex; gap: ${unsafeCSS(spacing[2])}; align-items: flex-end; } .message-input { flex: 1; min-height: 40px; max-height: 120px; padding: ${unsafeCSS(spacing[2])} ${unsafeCSS(spacing[3])}; background: ${bdTheme('background')}; border: 1px solid ${bdTheme('border')}; border-radius: ${unsafeCSS(radius.md)}; font-size: 14px; color: ${bdTheme('foreground')}; outline: none; resize: none; font-family: ${unsafeCSS(fontFamilies.sans)}; line-height: 1.5; transition: ${unsafeCSS(transitions.all)}; } .message-input::placeholder { color: ${bdTheme('mutedForeground')}; } .message-input:focus { border-color: ${bdTheme('ring')}; } .input-actions { display: flex; gap: ${unsafeCSS(spacing[1])}; } .empty-state { flex: 1; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: ${unsafeCSS(spacing[4])}; padding: ${unsafeCSS(spacing[8])}; text-align: center; color: ${bdTheme('mutedForeground')}; } .empty-icon { font-size: 64px; opacity: 0.5; } /* Scrollbar styling */ .messages-container::-webkit-scrollbar { width: 6px; } .messages-container::-webkit-scrollbar-track { background: transparent; } .messages-container::-webkit-scrollbar-thumb { background: ${bdTheme('border')}; border-radius: 3px; } .messages-container::-webkit-scrollbar-thumb:hover { background: ${bdTheme('mutedForeground')}; } `, ]; public render(): TemplateResult { if (!this.conversation) { return html`

Select a conversation

Choose a conversation from the sidebar to start messaging

`; } return html`

${this.conversation.title}

${this.conversation.messages.map(msg => html`
${msg.text}
${msg.time}
`)} ${this.isTyping ? html`
` : ''}
`; } private handleBack() { this.dispatchEvent(new CustomEvent('back', { bubbles: true, composed: true })); } private handleInput(e: Event) { const textarea = e.target as HTMLTextAreaElement; this.messageText = textarea.value; // Auto-resize textarea textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } private handleKeyDown(e: KeyboardEvent) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } } private sendMessage() { if (!this.messageText.trim()) return; const message: IMessage = { id: Date.now().toString(), text: this.messageText.trim(), sender: 'user', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }), status: 'sending' }; // Dispatch event for parent to handle this.dispatchEvent(new CustomEvent('send-message', { detail: { message }, bubbles: true, composed: true })); // Clear input this.messageText = ''; const textarea = this.shadowRoot?.querySelector('.message-input') as HTMLTextAreaElement; if (textarea) { textarea.style.height = 'auto'; } // Simulate typing indicator (remove in production) setTimeout(() => { this.isTyping = true; setTimeout(() => { this.isTyping = false; }, 2000); }, 1000); } public updated() { // Scroll to bottom when new messages arrive const container = this.shadowRoot?.querySelector('#messages'); if (container) { container.scrollTop = container.scrollHeight; } } }