import { DeesElement, property, html, customElement, type TemplateResult, cssManager, css, unsafeCSS, state, } from '@design.estate/dees-element'; import * as domtools from '@design.estate/dees-domtools'; import { SioCombox } from './sio-combox.js'; import { SioIcon } from './sio-icon.js'; SioCombox; SioIcon; // Import design tokens import { colors, bdTheme } from './00colors.js'; import { spacing, radius, shadows, transitions, sizes } from './00tokens.js'; import { fontFamilies, typography } from './00fonts.js'; declare global { interface HTMLElementTagNameMap { 'sio-fab': SioFab; } } @customElement('sio-fab') export class SioFab extends DeesElement { @property({ type: Boolean }) public accessor showCombox = false; @state() private accessor hasShownOnce = false; @state() private accessor shouldPulse = false; public static demo = () => html` `; constructor() { super(); domtools.DomTools.setupDomTools(); } public static styles = [ cssManager.defaultStyles, css` :host { will-change: transform; position: absolute; display: block; bottom: 20px; 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); --fab-size: 60px; --fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["4"])}); } #mainbox { transition: ${unsafeCSS(transitions.all)}; position: absolute; bottom: 0px; right: 0px; height: var(--fab-size); width: var(--fab-size); box-shadow: 0 4px 16px -2px rgba(0, 0, 0, 0.1), 0 2px 8px -2px rgba(0, 0, 0, 0.06); line-height: var(--fab-size); text-align: center; cursor: pointer; background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-end) 100%); color: white; border-radius: ${unsafeCSS(radius.full)}; user-select: none; border: none; animation: fabEntrance 300ms cubic-bezier(0.4, 0, 0.2, 1); overflow: hidden; 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.8); opacity: 0; } to { transform: scale(1); opacity: 1; } } #mainbox:hover { transform: scale(1.02); background: linear-gradient(135deg, var(--fab-gradient-start) 0%, var(--fab-gradient-mid) 50%, var(--fab-gradient-hover-end) 100%); box-shadow: 0 8px 20px -4px var(--fab-shadow-color); } #mainbox:active { 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, opacity; transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); height: 100%; width: 100%; display: flex; align-items: center; justify-content: center; } #mainbox .icon.open { opacity: 1; transform: rotate(0deg) scale(1); } #mainbox .icon.close { opacity: 0; transform: rotate(-45deg) scale(0.9); } /* When combox is open */ :host(.combox-open) #mainbox .icon.open { opacity: 0; transform: rotate(45deg) scale(0.9); } :host(.combox-open) #mainbox .icon.close { opacity: 1; transform: rotate(0deg) scale(1); } #mainbox .icon sio-icon { color: white; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); } #mainbox .icon.close sio-icon { transform: scale(1); } #comboxContainer { position: absolute; bottom: 0; right: 0; pointer-events: none; } #comboxContainer sio-combox { position: absolute; bottom: var(--fab-combox-offset); right: 0; transition: ${unsafeCSS(transitions.all)}; will-change: transform; transform: translateY(${unsafeCSS(spacing["5"])}); opacity: 0; pointer-events: none; } #comboxContainer.show { pointer-events: all; } #comboxContainer.show sio-combox { transform: translateY(0px); opacity: 1; pointer-events: all; } `, // Mobile responsive styles - smaller FAB and full-screen combox cssManager.cssForPhablet(css` :host { --fab-size: 48px; bottom: 16px; right: 16px; will-change: auto; } #comboxContainer { position: fixed; top: 0; left: 0; bottom: auto; right: auto; width: 100vw; height: 100vh; height: 100dvh; } #comboxContainer sio-combox { bottom: 0; right: 0; transform: none; } #comboxContainer.show sio-combox { transform: none; } `), ]; public render(): TemplateResult { return html`
{ this.shouldPulse = false; }} >
${this.showCombox || this.hasShownOnce ? html` this.showCombox = false}> ` : ''}
`; } /** * toggles the combox */ 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: any) { super.firstUpdated(args); const domtools = await this.domtoolsPromise; // Set up keyboard shortcut domtools.keyboard .on([domtools.keyboard.keyEnum.Ctrl, domtools.keyboard.keyEnum.S]) .subscribe(() => { this.toggleCombox(); }); } public async updated(changedProperties: Map) { super.updated(changedProperties); // Update host class based on combox state if (changedProperties.has('showCombox')) { if (this.showCombox) { this.classList.add('combox-open'); } else { this.classList.remove('combox-open'); } } // Set reference object when combox is rendered if ((changedProperties.has('showCombox') || changedProperties.has('hasShownOnce')) && (this.showCombox || this.hasShownOnce)) { const sioCombox: SioCombox = this.shadowRoot.querySelector('sio-combox'); const mainBox: HTMLElement = this.shadowRoot.querySelector('#mainbox'); if (sioCombox && mainBox && !sioCombox.referenceObject) { sioCombox.referenceObject = mainBox; } } } }