2024-12-27 01:53:26 +01:00
import {
DeesElement ,
property ,
html ,
customElement ,
type TemplateResult ,
2025-04-20 20:09:42 +00:00
cssManager ,
2025-07-14 14:54:54 +00:00
css ,
unsafeCSS ,
state ,
2024-12-27 01:53:26 +01:00
} from '@design.estate/dees-element' ;
import * as domtools from '@design.estate/dees-domtools' ;
2025-07-14 14:54:54 +00:00
// Import design tokens
import { colors , bdTheme } from './00colors.js' ;
import { spacing , radius , shadows , transitions } from './00tokens.js' ;
import { fontFamilies , typography } from './00fonts.js' ;
// Import components
import { SioConversationSelector , type IConversation } from './sio-conversation-selector.js' ;
2025-07-14 15:21:37 +00:00
import { SioConversationView , type IMessage , type IConversationData , type IAttachment } from './sio-conversation-view.js' ;
import { SioImageLightbox , type ILightboxImage } from './sio-image-lightbox.js' ;
2025-07-14 14:54:54 +00:00
// Make sure components are loaded
SioConversationSelector ;
SioConversationView ;
2025-07-14 15:21:37 +00:00
SioImageLightbox ;
2025-07-14 14:54:54 +00:00
declare global {
interface HTMLElementTagNameMap {
'sio-combox' : SioCombox ;
}
}
2024-12-27 01:53:26 +01:00
@customElement ( 'sio-combox' )
export class SioCombox extends DeesElement {
public static demo = ( ) = > html ` <sio-combox></sio-combox> ` ;
2025-12-17 11:41:47 +00:00
// Singleton instance
private static instance : SioCombox | null = null ;
/ * *
* Creates and appends a singleton combox to document . body
* /
public static createOnBody ( ) : SioCombox {
if ( ! SioCombox . instance ) {
SioCombox . instance = new SioCombox ( ) ;
document . body . appendChild ( SioCombox . instance ) ;
}
return SioCombox . instance ;
}
/ * *
* Gets the singleton instance if it exists
* /
public static getInstance ( ) : SioCombox | null {
return SioCombox . instance ;
}
2024-12-27 01:53:26 +01:00
@property ( { type : Object } )
2025-12-08 23:03:02 +00:00
public accessor referenceObject : HTMLElement ;
2024-12-27 01:53:26 +01:00
2025-07-14 14:54:54 +00:00
@state ( )
2025-12-08 23:03:02 +00:00
private accessor selectedConversationId : string | null = null ;
2024-12-27 01:53:26 +01:00
2025-12-17 10:07:18 +00:00
@state ( )
private accessor isKeyboardVisible : boolean = false ;
2025-12-17 11:41:47 +00:00
@state ( )
private accessor isOpen : boolean = false ;
2025-12-17 10:07:18 +00:00
private keyboardBlurTimeout? : number ;
2025-07-14 14:54:54 +00:00
@state ( )
2025-12-08 23:03:02 +00:00
private accessor conversations : IConversation [ ] = [
2025-07-14 14:54:54 +00:00
{
id : '1' ,
title : 'Technical Support' ,
lastMessage : 'Thanks for your help with the login issue!' ,
time : '2 min ago' ,
unread : true ,
2025-12-18 08:16:46 +00:00
status : 'new' ,
2025-07-14 14:54:54 +00:00
} ,
{
id : '2' ,
title : 'Billing Question' ,
lastMessage : 'I need help understanding my invoice' ,
time : '1 hour ago' ,
2025-12-18 08:16:46 +00:00
status : 'needs-action' ,
2025-07-14 14:54:54 +00:00
} ,
{
id : '3' ,
title : 'Feature Request' ,
lastMessage : 'That would be great! Looking forward to it' ,
time : 'Yesterday' ,
2025-12-18 08:16:46 +00:00
status : 'waiting' ,
2025-07-14 14:54:54 +00:00
} ,
{
id : '4' ,
title : 'General Inquiry' ,
lastMessage : 'Thank you for the information' ,
time : '2 days ago' ,
2025-12-18 08:16:46 +00:00
status : 'resolved' ,
2024-12-27 01:53:26 +01:00
}
2025-07-14 14:54:54 +00:00
] ;
@state ( )
2025-12-08 23:03:02 +00:00
private accessor messages : { [ conversationId : string ] : IMessage [ ] } = {
2025-07-14 14:54:54 +00:00
'1' : [
{ id : '1' , text : 'Hi, I\'m having trouble logging in' , sender : 'user' , time : '10:00 AM' } ,
{ id : '2' , text : 'I can help you with that. Can you tell me what error you\'re seeing?' , sender : 'support' , time : '10:02 AM' } ,
{ id : '3' , text : 'It says "Invalid credentials" but I\'m sure my password is correct' , sender : 'user' , time : '10:03 AM' } ,
{ id : '4' , text : 'Let me check your account. Please try resetting your password using the forgot password link.' , sender : 'support' , time : '10:05 AM' } ,
2025-07-14 15:21:37 +00:00
{
id : '5' ,
text : 'Here\'s a screenshot of the error' ,
sender : 'user' ,
time : '10:08 AM' ,
attachments : [ {
id : 'att1' ,
name : 'error-screenshot.png' ,
size : 245780 ,
type : 'image/png' ,
url : 'https://picsum.photos/400/300?random=1'
} ]
} ,
{ id : '6' , text : 'Thanks for your help with the login issue!' , sender : 'user' , time : '10:10 AM' } ,
2025-07-14 17:44:52 +00:00
{
id : '7' ,
text : 'Here is the documentation you requested' ,
sender : 'support' ,
time : '10:15 AM' ,
attachments : [ {
id : 'att2' ,
name : 'user-guide.pdf' ,
size : 2457600 ,
type : 'application/pdf' ,
url : 'data:application/pdf;base64,JVBERi0xLjMKJeLjz9MKMSAwIG9iago8PAovVHlwZSAvQ2F0YWxvZwovT3V0bGluZXMgMiAwIFIKL1BhZ2VzIDMgMCBSCj4+CmVuZG9iagoyIDAgb2JqCjw8Ci9UeXBlIC9PdXRsaW5lcwovQ291bnQgMAo+PgplbmRvYmoKMyAwIG9iago8PAovVHlwZSAvUGFnZXMKL0NvdW50IDEKL0tpZHMgWzQgMCBSXQo+PgplbmRvYmoKNCAwIG9iago8PAovVHlwZSAvUGFnZQovUGFyZW50IDMgMCBSCi9NZWRpYUJveCBbMCAwIDYxMiA3OTJdCi9Db250ZW50cyA1IDAgUgovUmVzb3VyY2VzIDw8Ci9Gb250IDw8Ci9GMSA2IDAgUgo+Pgo+Pgo+PgplbmRvYmoKNSAwIG9iago8PAovTGVuZ3RoIDQ0Cj4+CnN0cmVhbQpCVApxCjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8gV29ybGQpIFRqCkVUClEKZW5kc3RyZWFtCmVuZG9iago2IDAgb2JqCjw8Ci9UeXBlIC9Gb250Ci9TdWJ0eXBlIC9UeXBlMQovQmFzZUZvbnQgL1RpbWVzLVJvbWFuCj4+CmVuZG9iagp4cmVmCjAgNwowMDAwMDAwMDAwIDY1NTM1IGYgCjAwMDAwMDAwMDkgMDAwMDAgbiAKMDAwMDAwMDA3NCAwMDAwMCBuIAowMDAwMDAwMTIwIDAwMDAwIG4gCjAwMDAwMDAxNzkgMDAwMDAgbiAKMDAwMDAwMDM2NCAwMDAwMCBuIAowMDAwMDAwNDY2IDAwMDAwIG4gCnRyYWlsZXIKPDwKL1NpemUgNwovUm9vdCAxIDAgUgo+PgpzdGFydHhyZWYKNTY1CiUlRU9G'
} ]
} ,
2025-07-14 14:54:54 +00:00
] ,
'2' : [
{ id : '1' , text : 'I need help understanding my invoice' , sender : 'user' , time : '9:00 AM' } ,
{ id : '2' , text : 'I\'d be happy to help explain your invoice. Which part is unclear?' , sender : 'support' , time : '9:05 AM' } ,
] ,
'3' : [
{ id : '1' , text : 'I\'d love to see dark mode support in your app!' , sender : 'user' , time : 'Yesterday' } ,
{ id : '2' , text : 'Thanks for the suggestion! We\'re actually working on dark mode and it should be available next month.' , sender : 'support' , time : 'Yesterday' } ,
{ id : '3' , text : 'That would be great! Looking forward to it' , sender : 'user' , time : 'Yesterday' } ,
] ,
'4' : [
{ id : '1' , text : 'Can you tell me more about your enterprise plans?' , sender : 'user' , time : '2 days ago' } ,
{ id : '2' , text : 'Of course! Our enterprise plans include advanced features like SSO, dedicated support, and custom integrations.' , sender : 'support' , time : '2 days ago' } ,
{ id : '3' , text : 'Thank you for the information' , sender : 'user' , time : '2 days ago' } ,
]
} ;
2024-12-27 01:53:26 +01:00
constructor ( ) {
super ( ) ;
domtools . DomTools . setupDomTools ( ) ;
}
2025-12-17 10:07:18 +00:00
async connectedCallback() {
await super . connectedCallback ( ) ;
this . addEventListener ( 'input-focus' , this . handleInputFocus as EventListener ) ;
this . addEventListener ( 'input-blur' , this . handleInputBlur as EventListener ) ;
}
async disconnectedCallback() {
await super . disconnectedCallback ( ) ;
this . removeEventListener ( 'input-focus' , this . handleInputFocus as EventListener ) ;
this . removeEventListener ( 'input-blur' , this . handleInputBlur as EventListener ) ;
if ( this . keyboardBlurTimeout ) {
clearTimeout ( this . keyboardBlurTimeout ) ;
}
}
private handleInputFocus = ( ) = > {
if ( this . keyboardBlurTimeout ) {
clearTimeout ( this . keyboardBlurTimeout ) ;
this . keyboardBlurTimeout = undefined ;
}
this . isKeyboardVisible = true ;
}
private handleInputBlur = ( ) = > {
if ( this . keyboardBlurTimeout ) {
clearTimeout ( this . keyboardBlurTimeout ) ;
}
this . keyboardBlurTimeout = window . setTimeout ( ( ) = > {
this . isKeyboardVisible = false ;
this . keyboardBlurTimeout = undefined ;
} , 150 ) ;
}
updated ( changedProperties : Map < string , any > ) {
super . updated ( changedProperties ) ;
if ( changedProperties . has ( 'isKeyboardVisible' ) ) {
if ( this . isKeyboardVisible ) {
this . setAttribute ( 'keyboard-visible' , '' ) ;
} else {
this . removeAttribute ( 'keyboard-visible' ) ;
}
}
2025-12-17 11:41:47 +00:00
if ( changedProperties . has ( 'isOpen' ) ) {
if ( this . isOpen ) {
this . classList . add ( 'open' ) ;
this . dispatchEvent ( new CustomEvent ( 'opened' , { bubbles : true , composed : true } ) ) ;
} else {
this . classList . remove ( 'open' ) ;
this . dispatchEvent ( new CustomEvent ( 'closed' , { bubbles : true , composed : true } ) ) ;
}
}
}
/ * *
* Opens the combox
* /
public open() {
this . isOpen = true ;
}
/ * *
* Closes the combox
* /
public close() {
this . isOpen = false ;
this . dispatchEvent ( new CustomEvent ( 'close' , { bubbles : true , composed : true } ) ) ;
}
/ * *
* Toggles the combox open / closed state
* /
public toggle() {
if ( this . isOpen ) {
this . close ( ) ;
} else {
this . open ( ) ;
}
}
/ * *
* Returns whether the combox is currently open
* /
public getIsOpen ( ) : boolean {
return this . isOpen ;
2025-12-17 10:07:18 +00:00
}
2025-07-14 14:54:54 +00:00
public static styles = [
cssManager . defaultStyles ,
css `
: host {
display : block ;
2025-12-17 11:41:47 +00:00
position : fixed ;
bottom : 100px ;
right : 20px ;
2025-07-14 14:54:54 +00:00
height : 600px ;
width : 800px ;
background : $ { bdTheme ( 'background' ) } ;
2025-07-14 15:07:39 +00:00
border - radius : $ { unsafeCSS ( radius [ '2xl' ] ) } ;
2025-07-14 14:54:54 +00:00
border : 1px solid $ { bdTheme ( 'border' ) } ;
2025-12-18 08:28:40 +00:00
box - shadow : 0 25 px 50 px - 12 px rgb ( 0 0 0 / 0.35 ) , 0 12 px 24 px - 8 px rgb ( 0 0 0 / 0.15 ) ;
2025-07-14 17:44:52 +00:00
overflow : hidden ;
2025-07-14 14:54:54 +00:00
font - family : $ { unsafeCSS ( fontFamilies . sans ) } ;
2025-07-14 15:11:47 +00:00
transform - origin : bottom right ;
2025-12-17 11:41:47 +00:00
z - index : 10001 ;
/* Hidden by default */
opacity : 0 ;
pointer - events : none ;
transform : scale ( 0.95 ) translateY ( 10 px ) ;
transition : opacity 200 ms ease , transform 200 ms ease ;
2025-07-14 15:11:47 +00:00
}
2025-12-17 11:41:47 +00:00
: host ( . open ) {
opacity : 1 ;
pointer - events : all ;
transform : scale ( 1 ) translateY ( 0 ) ;
2025-07-14 15:07:39 +00:00
}
2025-12-17 11:41:47 +00:00
2025-07-14 15:07:39 +00:00
: host : : before {
content : '' ;
position : absolute ;
inset : 0 ;
border - radius : $ { unsafeCSS ( radius [ '2xl' ] ) } ;
padding : 1px ;
background : linear - gradient ( 145 deg , $ { bdTheme ( 'border' ) } , transparent 50 % ) ;
- webkit - mask : linear - gradient ( # fff 0 0 ) content - box , linear - gradient ( # fff 0 0 ) ;
- webkit - mask - composite : exclude ;
2025-07-14 15:30:16 +00:00
mask : linear - gradient ( # fff 0 0 ) content - box , linear - gradient ( # fff 0 0 ) ;
2025-07-14 15:07:39 +00:00
mask - composite : exclude ;
opacity : 0.5 ;
pointer - events : none ;
2025-07-14 14:54:54 +00:00
}
. container {
display : flex ;
height : 100 % ;
2025-07-14 17:26:57 +00:00
overflow : visible ;
border - radius : $ { unsafeCSS ( radius [ '2xl' ] ) } ;
2025-07-14 14:54:54 +00:00
}
2025-12-17 09:22:02 +00:00
/* Desktop layout (default) */
sio - conversation - selector {
width : 320px ;
flex - shrink : 0 ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
sio - conversation - view {
flex : 1 ;
}
` ,
// Mobile responsive layout - full screen with sliding mechanics
cssManager . cssForPhablet ( css `
: host {
2025-12-17 11:41:47 +00:00
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
width : 100vw ;
height : 100vh ;
height : 100dvh ;
2025-12-17 09:22:02 +00:00
border - radius : 0 ;
2025-12-17 11:41:47 +00:00
transform - origin : center center ;
}
: host ( . open ) {
transform : scale ( 1 ) translateY ( 0 ) ;
2025-12-17 09:22:02 +00:00
}
2025-07-14 14:54:54 +00:00
2025-12-17 09:22:02 +00:00
: host : : before {
border - radius : 0 ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
. container {
position : relative ;
overflow : hidden ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
sio - conversation - selector {
position : absolute ;
width : 100 % ;
height : 100 % ;
transition : left 300 ms ease , opacity 200 ms ease ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
sio - conversation - view {
position : absolute ;
width : 100 % ;
height : 100 % ;
transition : left 300 ms ease , opacity 200 ms ease ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
/* Mobile navigation states */
. container . show - list sio - conversation - selector {
left : 0 ;
opacity : 1 ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
. container . show - list sio - conversation - view {
left : 100 % ;
opacity : 0 ;
2025-07-14 14:54:54 +00:00
}
2025-12-17 09:22:02 +00:00
. container . show - conversation sio - conversation - selector {
left : - 100 % ;
opacity : 0 ;
}
2024-12-27 01:53:26 +01:00
2025-12-17 09:22:02 +00:00
. container . show - conversation sio - conversation - view {
left : 0 ;
opacity : 1 ;
2025-07-14 14:54:54 +00:00
}
2025-12-17 10:07:18 +00:00
/* Keyboard visible adjustments */
: host ( [ keyboard - visible ] ) {
height : 100vh ;
height : 100dvh ;
}
2025-12-17 09:22:02 +00:00
` ),
2025-07-14 14:54:54 +00:00
] ;
2024-12-27 01:53:26 +01:00
2025-07-14 14:54:54 +00:00
public render ( ) : TemplateResult {
const selectedConversation = this . selectedConversationId
? this . conversations . find ( c = > c . id === this . selectedConversationId )
: null ;
2024-12-27 01:53:26 +01:00
2025-07-14 14:54:54 +00:00
const conversationData : IConversationData | null = selectedConversation
? {
id : selectedConversation.id ,
title : selectedConversation.title ,
messages : this.messages [ selectedConversation . id ] || [ ]
2024-12-27 01:53:26 +01:00
}
2025-07-14 14:54:54 +00:00
: null ;
const containerClass = this . selectedConversationId ? 'show-conversation' : 'show-list' ;
return html `
< div class = "container ${containerClass}" >
< sio - conversation - selector
. conversations = $ { this . conversations }
. selectedConversationId = $ { this . selectedConversationId }
@conversation - selected = $ { this . handleConversationSelected }
2025-12-18 08:28:40 +00:00
@close = $ { ( ) = > this . close ( ) }
2025-07-14 14:54:54 +00:00
> < / s i o - c o n v e r s a t i o n - s e l e c t o r >
< sio - conversation - view
. conversation = $ { conversationData }
@back = $ { this . handleBack }
@send - message = $ { this . handleSendMessage }
2025-07-14 15:21:37 +00:00
@open - image = $ { this . handleOpenImage }
2025-07-14 17:44:52 +00:00
@open - file = $ { this . handleOpenImage }
2025-07-14 14:54:54 +00:00
> < / s i o - c o n v e r s a t i o n - v i e w >
2024-12-27 01:53:26 +01:00
< / div >
2025-07-14 15:21:37 +00:00
< sio - image - lightbox > < / s i o - i m a g e - l i g h t b o x >
2024-12-27 01:53:26 +01:00
` ;
}
2025-07-14 14:54:54 +00:00
private handleConversationSelected ( event : CustomEvent ) {
const conversation = event . detail . conversation as IConversation ;
this . selectedConversationId = conversation . id ;
// Mark conversation as read
const convIndex = this . conversations . findIndex ( c = > c . id === conversation . id ) ;
if ( convIndex !== - 1 ) {
this . conversations [ convIndex ] = { . . . this . conversations [ convIndex ] , unread : false } ;
this . conversations = [ . . . this . conversations ] ;
}
}
private handleBack() {
// For mobile view, go back to conversation list
this . selectedConversationId = null ;
}
private handleSendMessage ( event : CustomEvent ) {
const message = event . detail . message as IMessage ;
const conversationId = this . selectedConversationId ;
if ( conversationId ) {
// Add message to the conversation
if ( ! this . messages [ conversationId ] ) {
this . messages [ conversationId ] = [ ] ;
}
this . messages [ conversationId ] = [ . . . this . messages [ conversationId ] , message ] ;
this . messages = { . . . this . messages } ;
// Update conversation's last message
const convIndex = this . conversations . findIndex ( c = > c . id === conversationId ) ;
if ( convIndex !== - 1 ) {
this . conversations [ convIndex ] = {
. . . this . conversations [ convIndex ] ,
lastMessage : message.text ,
time : 'Just now'
} ;
// Move conversation to top
const [ conv ] = this . conversations . splice ( convIndex , 1 ) ;
this . conversations = [ conv , . . . this . conversations ] ;
}
// Simulate a response after a delay (remove in production)
2024-12-27 01:53:26 +01:00
setTimeout ( ( ) = > {
2025-07-14 14:54:54 +00:00
const responseMessage : IMessage = {
id : Date.now ( ) . toString ( ) ,
text : 'Thanks for your message! We\'ll get back to you shortly.' ,
sender : 'support' ,
time : new Date ( ) . toLocaleTimeString ( [ ] , { hour : '2-digit' , minute : '2-digit' } )
} ;
this . messages [ conversationId ] = [ . . . this . messages [ conversationId ] , responseMessage ] ;
this . messages = { . . . this . messages } ;
} , 3000 ) ;
}
2024-12-27 01:53:26 +01:00
}
2025-07-14 15:21:37 +00:00
private handleOpenImage ( event : CustomEvent ) {
const attachment = event . detail . attachment as IAttachment ;
const lightbox = this . shadowRoot ? . querySelector ( 'sio-image-lightbox' ) as SioImageLightbox ;
if ( lightbox && attachment ) {
2025-07-14 17:44:52 +00:00
const lightboxFile : ILightboxImage = {
2025-07-14 15:21:37 +00:00
url : attachment.url ,
name : attachment.name ,
2025-07-14 17:44:52 +00:00
size : attachment.size ,
type : attachment . type
2025-07-14 15:21:37 +00:00
} ;
2025-07-14 17:44:52 +00:00
lightbox . open ( lightboxFile ) ;
2025-07-14 15:21:37 +00:00
}
}
2025-07-14 14:54:54 +00:00
}