diff --git a/ts_web/elements/sio-combox.ts b/ts_web/elements/sio-combox.ts index 8f856ca..cfb1b55 100644 --- a/ts_web/elements/sio-combox.ts +++ b/ts_web/elements/sio-combox.ts @@ -92,6 +92,19 @@ export class SioCombox extends DeesElement { }] }, { id: '6', text: 'Thanks for your help with the login issue!', sender: 'user', time: '10:10 AM' }, + { + 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' + }] + }, ], '2': [ { id: '1', text: 'I need help understanding my invoice', sender: 'user', time: '9:00 AM' }, @@ -125,7 +138,7 @@ export class SioCombox extends DeesElement { border-radius: ${unsafeCSS(radius['2xl'])}; border: 1px solid ${bdTheme('border')}; box-shadow: ${unsafeCSS(shadows.xl)}; - overflow: visible; + overflow: hidden; font-family: ${unsafeCSS(fontFamilies.sans)}; position: relative; transform-origin: bottom right; @@ -257,6 +270,7 @@ export class SioCombox extends DeesElement { @back=${this.handleBack} @send-message=${this.handleSendMessage} @open-image=${this.handleOpenImage} + @open-file=${this.handleOpenImage} > @@ -326,12 +340,13 @@ export class SioCombox extends DeesElement { const lightbox = this.shadowRoot?.querySelector('sio-image-lightbox') as SioImageLightbox; if (lightbox && attachment) { - const lightboxImage: ILightboxImage = { + const lightboxFile: ILightboxImage = { url: attachment.url, name: attachment.name, - size: attachment.size + size: attachment.size, + type: attachment.type }; - lightbox.open(lightboxImage); + lightbox.open(lightboxFile); } } } \ No newline at end of file diff --git a/ts_web/elements/sio-conversation-view.ts b/ts_web/elements/sio-conversation-view.ts index 0b7114a..3cd21cb 100644 --- a/ts_web/elements/sio-conversation-view.ts +++ b/ts_web/elements/sio-conversation-view.ts @@ -553,6 +553,14 @@ export class SioConversationView extends DeesElement {
this.openImage(attachment)}> ${attachment.name}
+ ` : attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf') ? html` +
this.openImage(attachment)}> + +
+
${attachment.name}
+
${this.formatFileSize(attachment.size)}
+
+
` : html`
this.downloadFile(attachment)}> @@ -810,11 +818,20 @@ export class SioConversationView extends DeesElement { } private openImage(attachment: IAttachment) { - this.dispatchEvent(new CustomEvent('open-image', { - detail: { attachment }, - bubbles: true, - composed: true - })); + // Check if it's actually a PDF + if (attachment.type?.includes('pdf') || attachment.name?.toLowerCase().endsWith('.pdf')) { + this.dispatchEvent(new CustomEvent('open-file', { + detail: { attachment }, + bubbles: true, + composed: true + })); + } else { + this.dispatchEvent(new CustomEvent('open-image', { + detail: { attachment }, + bubbles: true, + composed: true + })); + } } private downloadFile(attachment: IAttachment) { diff --git a/ts_web/elements/sio-fab.ts b/ts_web/elements/sio-fab.ts index 140de31..45d1864 100644 --- a/ts_web/elements/sio-fab.ts +++ b/ts_web/elements/sio-fab.ts @@ -34,6 +34,9 @@ export class SioFab extends DeesElement { @state() private hasShownOnce = false; + @state() + private shouldPulse = false; + public static demo = () => html` `; constructor() { @@ -53,6 +56,11 @@ export class SioFab extends DeesElement { right: 20px; z-index: 10000; color: #fff; + --fab-gradient-start: #6366f1; + --fab-gradient-mid: #8b5cf6; + --fab-gradient-end: #a855f7; + --fab-gradient-hover-end: #c026d3; + --fab-shadow-color: rgba(139, 92, 246, 0.25); } #mainbox { @@ -60,101 +68,130 @@ export class SioFab extends DeesElement { position: absolute; bottom: 0px; right: 0px; - height: 56px; - width: 56px; - box-shadow: ${cssManager.bdTheme(shadows.md, shadows.lg)}; - line-height: 56px; + height: 60px; + width: 60px; + box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06); + line-height: 60px; text-align: center; cursor: pointer; - background: ${bdTheme('primary')}; - color: ${bdTheme('primaryForeground')}; + background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-end) 100%); + color: white; border-radius: ${radius.full}; user-select: none; - border: 1px solid ${bdTheme('border')}; - animation: fabEntrance 500ms cubic-bezier(0.34, 1.56, 0.64, 1); + border: none; + animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; + position: relative; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + } + + #mainbox::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 50%); + opacity: 0; + transition: opacity 200ms ease; + } + + #mainbox::after { + content: ''; + position: absolute; + top: -4px; + left: -4px; + right: -4px; + bottom: -4px; + background: linear-gradient(135deg, var(--fab-gradient-start), var(--fab-gradient-end)); + border-radius: inherit; + z-index: -1; + opacity: 0; + filter: blur(12px); + transition: opacity 300ms ease; + } + + #mainbox:hover::before { + opacity: 1; + } + + #mainbox:hover::after { + opacity: 0.3; } @keyframes fabEntrance { from { - transform: scale(0) rotate(-180deg); + transform: scale(0.8); opacity: 0; } to { - transform: scale(1) rotate(0deg); + transform: scale(1); opacity: 1; } } #mainbox:hover { - box-shadow: ${cssManager.bdTheme(shadows.lg, shadows.xl)}; - transform: translateY(-2px) scale(1.05); + transform: scale(1.02); + background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-hover-end) 100%); + } + + #mainbox:hover { + box-shadow: 0 8px 20px -4px var(--fab-shadow-color); } #mainbox:active { - transform: translateY(0) scale(0.98); - box-shadow: ${cssManager.bdTheme(shadows.sm, shadows.md)}; + transform: scale(0.98); + box-shadow: 0 4px 12px -2px var(--fab-shadow-color); + } + + #mainbox.pulse::after { + animation: fabPulse 0.6s ease-out forwards; + } + + @keyframes fabPulse { + 0% { + box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); + opacity: 1; + } + 100% { + box-shadow: 0 0 0 12px rgba(139, 92, 246, 0); + opacity: 0; + } } #mainbox .icon { position: absolute; top: 0px; left: 0px; - will-change: transform; - transform: ${this.showCombox ? 'rotate(180deg)' : 'rotate(0deg)'}; - transition: transform 300ms cubic-bezier(0.68, -0.55, 0.265, 1.55); + will-change: transform, opacity; + transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); height: 100%; width: 100%; - object-fit: contain; - -webkit-user-drag: none; - -khtml-user-drag: none; - -moz-user-drag: none; - -o-user-drag: none; + display: flex; + align-items: center; + justify-content: center; } - #mainbox .icon img { - filter: ${cssManager.bdTheme('brightness(0) invert(1)', 'brightness(1)')}; - position: absolute; - width: 100%; - top: 0px; - left: 0px; - will-change: transform; - transform: scale(0.2, 0.2) translateY(-5px); - } - #mainbox .icon.open:hover img { - filter: ${cssManager.bdTheme('brightness(0) invert(1)', 'brightness(1.2)')}; - } #mainbox .icon.open { opacity: ${this.showCombox ? '0' : '1'}; - pointer-events: ${this.showCombox ? 'none' : 'all'}; + transform: ${this.showCombox ? 'rotate(45deg) scale(0.9)' : 'rotate(0deg) scale(1)'}; } #mainbox .icon.close { opacity: ${this.showCombox ? '1' : '0'}; - pointer-events: ${this.showCombox ? 'all' : 'none'}; - } - #mainbox .icon.close:hover sio-icon { - color: ${bdTheme('primaryForeground')}; + transform: ${this.showCombox ? 'rotate(0deg) scale(1)' : 'rotate(-45deg) scale(0.9)'}; } - #mainbox .icon.open sio-icon { - position: absolute; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - color: ${bdTheme('primaryForeground')}; + #mainbox .icon sio-icon { + color: white; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); } #mainbox .icon.close sio-icon { - position: absolute; - width: 100%; - height: 100%; - display: flex; - align-items: center; - justify-content: center; - color: ${bdTheme('primaryForeground')}; + transform: scale(1); } #comboxContainer { @@ -166,7 +203,7 @@ export class SioFab extends DeesElement { #comboxContainer sio-combox { position: absolute; - bottom: calc(56px + ${spacing["4"]}); + bottom: calc(60px + ${spacing["4"]}); right: 0; transition: ${transitions.all}; will-change: transform; @@ -185,10 +222,13 @@ export class SioFab extends DeesElement { pointer-events: all; } -
+
{ this.shouldPulse = false; }} + >
-
@@ -207,13 +247,17 @@ export class SioFab extends DeesElement { */ public async toggleCombox() { console.log('toggle combox'); + const wasOpen = this.showCombox; this.showCombox = !this.showCombox; if (this.showCombox) { this.hasShownOnce = true; + if (!wasOpen) { + this.shouldPulse = true; + } } } - public async firstUpdated(args) { + public async firstUpdated(args: any) { super.firstUpdated(args); const domtools = await this.domtoolsPromise; const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox'); @@ -222,7 +266,7 @@ export class SioFab extends DeesElement { domtools.keyboard .on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S]) - .subscribe((event) => { + .subscribe(() => { this.toggleCombox(); }); } diff --git a/ts_web/elements/sio-image-lightbox.ts b/ts_web/elements/sio-image-lightbox.ts index 8ae317f..b4f5c4f 100644 --- a/ts_web/elements/sio-image-lightbox.ts +++ b/ts_web/elements/sio-image-lightbox.ts @@ -15,12 +15,16 @@ import { colors, bdTheme } from './00colors.js'; import { spacing, radius, shadows, transitions } from './00tokens.js'; import { fontFamilies } from './00fonts.js'; -export interface ILightboxImage { +export interface ILightboxFile { url: string; name: string; size?: number; + type?: string; } +// For backwards compatibility +export type ILightboxImage = ILightboxFile; + declare global { interface HTMLElementTagNameMap { 'sio-image-lightbox': SioImageLightbox; @@ -30,9 +34,10 @@ declare global { @customElement('sio-image-lightbox') export class SioImageLightbox extends DeesElement { public static demo = () => html` - `; @@ -40,10 +45,18 @@ export class SioImageLightbox extends DeesElement { public isOpen: boolean = false; @property({ type: Object }) - public image: ILightboxImage | null = null; + public file: ILightboxFile | null = null; + + // For backwards compatibility + public get image(): ILightboxFile | null { + return this.file; + } + public set image(value: ILightboxFile | null) { + this.file = value; + } @state() - private imageLoaded: boolean = false; + private fileLoaded: boolean = false; @state() private scale: number = 1; @@ -108,7 +121,7 @@ export class SioImageLightbox extends DeesElement { pointer-events: all; } - .image-wrapper { + .content-wrapper { position: relative; max-width: 90vw; max-height: 90vh; @@ -118,10 +131,14 @@ export class SioImageLightbox extends DeesElement { z-index: 1; } - .image-wrapper.dragging { + .content-wrapper.dragging { cursor: grabbing; transition: none; } + + .content-wrapper.pdf { + cursor: default; + } .image { display: block; @@ -136,6 +153,21 @@ export class SioImageLightbox extends DeesElement { .image.loaded { opacity: 1; } + + .pdf-viewer { + width: 90vw; + height: 90vh; + border-radius: ${unsafeCSS(radius.lg)}; + box-shadow: ${unsafeCSS(shadows["2xl"])}; + background: white; + opacity: 0; + transition: opacity 300ms ease; + border: none; + } + + .pdf-viewer.loaded { + opacity: 1; + } .loading { position: absolute; @@ -277,59 +309,79 @@ export class SioImageLightbox extends DeesElement { `, ]; + private isPDF(): boolean { + return this.file?.type?.includes('pdf') || + this.file?.name?.toLowerCase().endsWith('.pdf') || false; + } + public render(): TemplateResult { - const imageStyle = this.scale !== 1 || this.translateX !== 0 || this.translateY !== 0 + const contentStyle = this.scale !== 1 || this.translateX !== 0 || this.translateY !== 0 ? `transform: scale(${this.scale}) translate(${this.translateX}px, ${this.translateY}px)` : ''; + const isPDF = this.isPDF(); + return html`
{ if (e.target === e.currentTarget) this.close(e); }}>
- ${this.image ? html` + ${this.file ? html`
-
- -
-
- -
-
- -
+ ${!isPDF ? html` +
+ +
+
+ +
+
+ +
+ ` : ''}
this.close(e)}>
- ${!this.imageLoaded ? html` + ${!this.fileLoaded ? html`
Loading...
` : ''} - ${this.image.name} this.imageLoaded = true} - @error=${() => this.imageLoaded = false} - @click=${(e: Event) => e.stopPropagation()} - /> + ${isPDF ? html` + + ` : html` + ${this.file.name} this.fileLoaded = true} + @error=${() => this.fileLoaded = false} + @click=${(e: Event) => e.stopPropagation()} + /> + `}
-
${this.image.name}
+
${this.file.name}