Files
dees-catalog-mobile/ts_web/elements/00group-ui/dees-mobile-toast/dees-mobile-toast.ts
2025-12-22 10:53:15 +00:00

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>
`;
}
}