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 } from './00fonts.js'; export interface ILightboxImage { url: string; name: string; size?: number; } declare global { interface HTMLElementTagNameMap { 'sio-image-lightbox': SioImageLightbox; } } @customElement('sio-image-lightbox') export class SioImageLightbox extends DeesElement { public static demo = () => html` `; @property({ type: Boolean }) public isOpen: boolean = false; @property({ type: Object }) public image: ILightboxImage | null = null; @state() private imageLoaded: boolean = false; @state() private scale: number = 1; @state() private translateX: number = 0; @state() private translateY: number = 0; private isDragging: boolean = false; private startX: number = 0; private startY: number = 0; public static styles = [ cssManager.defaultStyles, css` :host { position: fixed; inset: 0; z-index: 10000; pointer-events: none; font-family: ${unsafeCSS(fontFamilies.sans)}; } .overlay { position: absolute; inset: 0; background: rgba(0, 0, 0, 0); backdrop-filter: blur(0px); -webkit-backdrop-filter: blur(0px); transition: all 300ms ease; pointer-events: none; opacity: 0; } .overlay.open { background: rgba(0, 0, 0, 0.9); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); pointer-events: all; opacity: 1; } .container { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: ${unsafeCSS(spacing["8"])}; pointer-events: none; opacity: 0; transform: scale(0.9); transition: all 300ms cubic-bezier(0.34, 1.56, 0.64, 1); } .container.open { opacity: 1; transform: scale(1); pointer-events: all; } .image-wrapper { position: relative; max-width: 90vw; max-height: 90vh; cursor: grab; user-select: none; transition: transform 100ms ease-out; } .image-wrapper.dragging { cursor: grabbing; transition: none; } .image { display: block; max-width: 100%; max-height: 90vh; border-radius: ${unsafeCSS(radius.lg)}; box-shadow: ${unsafeCSS(shadows["2xl"])}; opacity: 0; transition: opacity 300ms ease; } .image.loaded { opacity: 1; } .loading { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: white; display: flex; align-items: center; gap: ${unsafeCSS(spacing["2"])}; } .spinner { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .controls { position: absolute; top: ${unsafeCSS(spacing["4"])}; right: ${unsafeCSS(spacing["4"])}; display: flex; gap: ${unsafeCSS(spacing["2"])}; opacity: 0; transition: opacity 200ms ease; } .container.open .controls { opacity: 1; } .control-button { width: 40px; height: 40px; border-radius: ${unsafeCSS(radius.full)}; background: rgba(0, 0, 0, 0.5); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); color: white; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: ${unsafeCSS(transitions.all)}; } .control-button:hover { background: rgba(0, 0, 0, 0.7); transform: scale(1.1); } .control-button:active { transform: scale(0.95); } .info { position: absolute; bottom: ${unsafeCSS(spacing["4"])}; left: ${unsafeCSS(spacing["4"])}; right: ${unsafeCSS(spacing["4"])}; background: rgba(0, 0, 0, 0.7); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: ${unsafeCSS(radius.lg)}; padding: ${unsafeCSS(spacing["3"])} ${unsafeCSS(spacing["4"])}; color: white; display: flex; justify-content: space-between; align-items: center; opacity: 0; transform: translateY(10px); transition: all 200ms ease; } .container.open .info { opacity: 1; transform: translateY(0); } .info-name { font-weight: 500; font-size: 0.9375rem; } .info-actions { display: flex; gap: ${unsafeCSS(spacing["3"])}; } .info-button { background: none; border: none; color: white; opacity: 0.8; cursor: pointer; display: flex; align-items: center; gap: ${unsafeCSS(spacing["1"])}; font-size: 0.875rem; padding: ${unsafeCSS(spacing["1"])} ${unsafeCSS(spacing["2"])}; border-radius: ${unsafeCSS(radius.md)}; transition: ${unsafeCSS(transitions.all)}; } .info-button:hover { opacity: 1; background: rgba(255, 255, 255, 0.1); } @media (max-width: 600px) { .container { padding: ${unsafeCSS(spacing["4"])}; } .controls { top: ${unsafeCSS(spacing["2"])}; right: ${unsafeCSS(spacing["2"])}; } .info { bottom: ${unsafeCSS(spacing["2"])}; left: ${unsafeCSS(spacing["2"])}; right: ${unsafeCSS(spacing["2"])}; padding: ${unsafeCSS(spacing["2"])} ${unsafeCSS(spacing["3"])}; } } `, ]; public render(): TemplateResult { const imageStyle = this.scale !== 1 || this.translateX !== 0 || this.translateY !== 0 ? `transform: scale(${this.scale}) translate(${this.translateX}px, ${this.translateY}px)` : ''; return html`
${this.image ? html`
${!this.imageLoaded ? html`
Loading...
` : ''} ${this.image.name} this.imageLoaded = true} @error=${() => this.imageLoaded = false} @click=${(e: Event) => e.stopPropagation()} />
${this.image.name}
` : ''}
`; } public async open(image: ILightboxImage) { this.image = image; this.imageLoaded = false; this.resetZoom(); this.isOpen = true; // Add keyboard listener document.addEventListener('keydown', this.handleKeyDown); } private close = () => { this.isOpen = false; document.removeEventListener('keydown', this.handleKeyDown); // Dispatch close event this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })); // Clean up after animation setTimeout(() => { this.image = null; this.imageLoaded = false; this.resetZoom(); }, 300); } private handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape') { this.close(); } else if (e.key === '+' || e.key === '=') { this.zoomIn(); } else if (e.key === '-') { this.zoomOut(); } else if (e.key === '0') { this.resetZoom(); } } private zoomIn() { this.scale = Math.min(this.scale * 1.2, 3); } private zoomOut() { this.scale = Math.max(this.scale / 1.2, 0.5); } private resetZoom() { this.scale = 1; this.translateX = 0; this.translateY = 0; } private handleWheel = (e: WheelEvent) => { e.preventDefault(); if (e.ctrlKey || e.metaKey) { // Zoom with ctrl/cmd + scroll if (e.deltaY < 0) { this.zoomIn(); } else { this.zoomOut(); } } } private startDrag = (e: MouseEvent) => { if (this.scale > 1) { this.isDragging = true; this.startX = e.clientX - this.translateX; this.startY = e.clientY - this.translateY; e.preventDefault(); } } private drag = (e: MouseEvent) => { if (this.isDragging && this.scale > 1) { this.translateX = e.clientX - this.startX; this.translateY = e.clientY - this.startY; } } private endDrag = () => { this.isDragging = false; } private download() { if (!this.image) return; const a = document.createElement('a'); a.href = this.image.url; a.download = this.image.name; document.body.appendChild(a); a.click(); document.body.removeChild(a); } private openInNewTab() { if (!this.image) return; window.open(this.image.url, '_blank'); } public async disconnectedCallback() { await super.disconnectedCallback(); document.removeEventListener('keydown', this.handleKeyDown); } }