Files
catalog/ts_web/elements/sio-fab.ts

316 lines
8.1 KiB
TypeScript

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` <sio-fab .showCombox=${true}></sio-fab> `;
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 on phablet and phone
cssManager.cssForPhablet(css`
:host {
--fab-size: 48px;
--fab-combox-offset: calc(var(--fab-size) + ${unsafeCSS(spacing["3"])});
bottom: 16px;
right: 16px;
}
`),
];
public render(): TemplateResult {
return html`
<div id="mainbox"
class="${this.shouldPulse ? 'pulse' : ''}"
@click=${this.toggleCombox}
@animationend=${() => { this.shouldPulse = false; }}
>
<div class="icon open">
<sio-icon icon="message-square" size="24"></sio-icon>
</div>
<div class="icon close">
<sio-icon icon="x" size="20"></sio-icon>
</div>
</div>
<div id="comboxContainer" class="${this.showCombox ? 'show' : ''}">
${this.showCombox || this.hasShownOnce ? html`
<sio-combox @close=${() => this.showCombox = false}></sio-combox>
` : ''}
</div>
`;
}
/**
* 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<string | number | symbol, unknown>) {
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;
}
}
}
}