340 lines
7.7 KiB
TypeScript
340 lines
7.7 KiB
TypeScript
import {
|
|
DeesElement,
|
|
css,
|
|
cssManager,
|
|
customElement,
|
|
html,
|
|
property,
|
|
type TemplateResult,
|
|
} from '@design.estate/dees-element';
|
|
|
|
import { mobileComponentStyles } from '../../00componentstyles.js';
|
|
import '../dees-mobile-icon/dees-mobile-icon.js';
|
|
import { demoFunc } from './dees-mobile-toast.demo.js';
|
|
|
|
export type ToastType = 'success' | 'error' | 'info' | 'warning';
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
'dees-mobile-toast': DeesMobileToast;
|
|
}
|
|
}
|
|
|
|
@customElement('dees-mobile-toast')
|
|
export class DeesMobileToast extends DeesElement {
|
|
public static demo = demoFunc;
|
|
|
|
@property({ type: String })
|
|
accessor message: string = '';
|
|
|
|
@property({ type: String })
|
|
accessor type: ToastType = 'info';
|
|
|
|
@property({ type: Number })
|
|
accessor duration: number = 0; // 0 means use default
|
|
|
|
private timeoutId?: number;
|
|
|
|
public static styles = [
|
|
cssManager.defaultStyles,
|
|
mobileComponentStyles,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
position: fixed;
|
|
/* Mobile-first defaults */
|
|
bottom: 1rem;
|
|
left: 1rem;
|
|
right: 1rem;
|
|
transform: none;
|
|
z-index: var(--dees-z-notification, 900);
|
|
animation: slideUp 200ms var(--dees-spring);
|
|
}
|
|
|
|
/* Desktop enhancements */
|
|
@media (min-width: 641px) {
|
|
:host {
|
|
bottom: 2rem;
|
|
left: 50%;
|
|
right: auto;
|
|
transform: translateX(-50%);
|
|
}
|
|
}
|
|
|
|
/* Mobile-first animations */
|
|
@keyframes slideUp {
|
|
from {
|
|
transform: translateY(100%) scale(0.95);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translateY(0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
transform: translateY(0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translateY(100%) scale(0.95);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
/* Desktop-specific animations that include X translation */
|
|
@media (min-width: 641px) {
|
|
@keyframes slideUp {
|
|
from {
|
|
transform: translate(-50%, 100%) scale(0.95);
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
transform: translate(-50%, 0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
@keyframes slideDown {
|
|
from {
|
|
transform: translate(-50%, 0) scale(1);
|
|
opacity: 1;
|
|
}
|
|
to {
|
|
transform: translate(-50%, 100%) scale(0.95);
|
|
opacity: 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
:host(.closing) {
|
|
animation: slideDown 200ms var(--dees-ease-in);
|
|
}
|
|
|
|
.toast {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 1rem 1.5rem;
|
|
border-radius: var(--dees-radius);
|
|
box-shadow: var(--dees-shadow-lg);
|
|
/* Mobile-first defaults */
|
|
width: 100%;
|
|
min-width: auto;
|
|
max-width: none;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Desktop enhancements */
|
|
@media (min-width: 641px) {
|
|
.toast {
|
|
width: auto;
|
|
min-width: 300px;
|
|
max-width: 500px;
|
|
}
|
|
}
|
|
|
|
/* Type-specific styles */
|
|
.toast.success {
|
|
background: var(--dees-card);
|
|
color: var(--dees-foreground);
|
|
border: 1px solid var(--dees-border);
|
|
}
|
|
|
|
.toast.error {
|
|
background: var(--dees-danger);
|
|
color: white;
|
|
border: 1px solid var(--dees-danger);
|
|
}
|
|
|
|
.toast.warning {
|
|
background: var(--dees-warning);
|
|
color: white;
|
|
border: 1px solid var(--dees-warning);
|
|
}
|
|
|
|
.toast.info {
|
|
background: var(--dees-primary);
|
|
color: white;
|
|
border: 1px solid var(--dees-primary);
|
|
}
|
|
|
|
.icon {
|
|
flex-shrink: 0;
|
|
width: 1.25rem;
|
|
height: 1.25rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.icon.success {
|
|
color: var(--dees-success);
|
|
}
|
|
|
|
.icon.error,
|
|
.icon.warning,
|
|
.icon.info {
|
|
color: currentColor;
|
|
}
|
|
|
|
.message {
|
|
flex: 1;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.close {
|
|
flex-shrink: 0;
|
|
width: 2rem;
|
|
height: 2rem;
|
|
padding: 0.375rem;
|
|
margin: -0.375rem;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: all var(--dees-transition-fast);
|
|
opacity: 0.8;
|
|
position: relative;
|
|
z-index: 2;
|
|
pointer-events: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.close.success {
|
|
color: var(--dees-muted-foreground);
|
|
}
|
|
|
|
.close.error,
|
|
.close.warning,
|
|
.close.info {
|
|
color: currentColor;
|
|
}
|
|
|
|
.close:hover {
|
|
opacity: 1;
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
/* Progress bar */
|
|
.progress-bar {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 3px;
|
|
background: currentColor;
|
|
opacity: 0.3;
|
|
transform-origin: left;
|
|
animation: progress linear forwards;
|
|
pointer-events: none;
|
|
transform: scaleX(1);
|
|
}
|
|
|
|
.toast.success .progress-bar {
|
|
background: var(--dees-success);
|
|
}
|
|
|
|
.toast.error .progress-bar,
|
|
.toast.warning .progress-bar,
|
|
.toast.info .progress-bar {
|
|
background: rgba(255, 255, 255, 0.5);
|
|
}
|
|
|
|
@keyframes progress {
|
|
from {
|
|
transform: scaleX(1);
|
|
}
|
|
to {
|
|
transform: scaleX(0);
|
|
}
|
|
}
|
|
`,
|
|
];
|
|
|
|
private get defaultDuration(): number {
|
|
switch (this.type) {
|
|
case 'success': return 3000;
|
|
case 'error': return 5000;
|
|
case 'warning': return 4000;
|
|
case 'info': return 4000;
|
|
}
|
|
}
|
|
|
|
async connectedCallback() {
|
|
await super.connectedCallback();
|
|
// Auto-dismiss after duration
|
|
const duration = this.duration || this.defaultDuration;
|
|
this.timeoutId = window.setTimeout(() => {
|
|
this.handleClose();
|
|
}, duration);
|
|
}
|
|
|
|
async disconnectedCallback() {
|
|
await super.disconnectedCallback();
|
|
// Clear the timeout when the element is removed
|
|
if (this.timeoutId) {
|
|
clearTimeout(this.timeoutId);
|
|
}
|
|
}
|
|
|
|
private handleClose() {
|
|
// Cancel the auto-dismiss timer
|
|
if (this.timeoutId) {
|
|
clearTimeout(this.timeoutId);
|
|
this.timeoutId = undefined;
|
|
}
|
|
|
|
// Prevent double-triggering
|
|
if (this.classList.contains('closing')) return;
|
|
|
|
// Add closing animation
|
|
this.classList.add('closing');
|
|
|
|
// Wait for closing animation to complete
|
|
setTimeout(() => {
|
|
this.dispatchEvent(new CustomEvent('close', {
|
|
bubbles: true,
|
|
composed: true,
|
|
}));
|
|
}, 200);
|
|
}
|
|
|
|
private getIcon(): string {
|
|
switch (this.type) {
|
|
case 'success': return 'check-circle';
|
|
case 'error': return 'alert-circle';
|
|
case 'warning': return 'alert-triangle';
|
|
case 'info': return 'info';
|
|
}
|
|
}
|
|
|
|
public render(): TemplateResult {
|
|
const duration = this.duration || this.defaultDuration;
|
|
|
|
return html`
|
|
<div class="toast ${this.type}">
|
|
<div class="icon ${this.type}">
|
|
<dees-mobile-icon icon=${this.getIcon()} size="20"></dees-mobile-icon>
|
|
</div>
|
|
<span class="message">${this.message}</span>
|
|
<button
|
|
class="close ${this.type}"
|
|
@click=${() => this.handleClose()}
|
|
aria-label="Close"
|
|
type="button"
|
|
>
|
|
<dees-mobile-icon icon="x" size="20"></dees-mobile-icon>
|
|
</button>
|
|
<div class="progress-bar" style="animation-duration: ${duration}ms"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|