Files
dss/admin-ui/js/components/ds-toast.js
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

168 lines
4.9 KiB
JavaScript

/**
* admin-ui/js/components/ds-toast.js
* A single toast notification component with swipe-to-dismiss support.
*/
class DsToast extends HTMLElement {
static get observedAttributes() {
return ['type', 'duration'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._duration = 5000;
this._dismissTimer = null;
}
connectedCallback() {
this.render();
this.setupAutoDismiss();
this.setupSwipeToDismiss();
this.shadowRoot.querySelector('.close-button')?.addEventListener('click', () => this.dismiss());
}
disconnectedCallback() {
if (this._dismissTimer) {
clearTimeout(this._dismissTimer);
}
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'duration') {
this._duration = parseInt(newValue, 10);
}
}
setupAutoDismiss() {
if (this._duration > 0 && !this.hasAttribute('progress')) {
this._dismissTimer = setTimeout(() => this.dismiss(), this._duration);
}
}
dismiss() {
if (this._dismissTimer) {
clearTimeout(this._dismissTimer);
}
this.classList.add('dismissing');
this.addEventListener('animationend', () => {
this.dispatchEvent(new CustomEvent('dismiss', { bubbles: true, composed: true }));
this.remove();
}, { once: true });
}
setupSwipeToDismiss() {
let startX = 0;
let currentX = 0;
let isDragging = false;
this.addEventListener('pointerdown', (e) => {
isDragging = true;
startX = e.clientX;
currentX = startX;
this.style.transition = 'none';
this.setPointerCapture(e.pointerId);
});
this.addEventListener('pointermove', (e) => {
if (!isDragging) return;
currentX = e.clientX;
const diff = currentX - startX;
this.style.transform = `translateX(${diff}px)`;
});
const onPointerUp = (e) => {
if (!isDragging) return;
isDragging = false;
this.style.transition = 'transform 0.2s ease';
const diff = currentX - startX;
const threshold = this.offsetWidth * 0.3;
if (Math.abs(diff) > threshold) {
this.style.transform = `translateX(${diff > 0 ? '100%' : '-100%'})`;
this.dismiss();
} else {
this.style.transform = 'translateX(0)';
}
};
this.addEventListener('pointerup', onPointerUp);
this.addEventListener('pointercancel', onPointerUp);
}
render() {
const type = this.getAttribute('type') || 'info';
const dismissible = this.hasAttribute('dismissible');
this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
align-items: center;
gap: var(--space-3);
background: var(--card);
color: var(--card-foreground);
border: 1px solid var(--border);
border-left: 4px solid var(--primary);
padding: var(--space-3) var(--space-4);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
transform-origin: top center;
animation: slide-in 0.3s ease forwards;
will-change: transform, opacity;
cursor: grab;
touch-action: pan-y;
}
:host([type="success"]) { border-left-color: var(--success); }
:host([type="warning"]) { border-left-color: var(--warning); }
:host([type="error"]) { border-left-color: var(--destructive); }
:host(.dismissing) {
animation: slide-out 0.3s ease forwards;
}
.content {
flex: 1;
font-size: var(--text-sm);
line-height: 1.4;
}
.close-button {
background: none;
border: none;
color: var(--muted-foreground);
padding: var(--space-1);
cursor: pointer;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: var(--radius);
transition: background 0.15s ease;
}
.close-button:hover {
background: var(--accent);
color: var(--foreground);
}
@keyframes slide-in {
from { opacity: 0; transform: translateY(-20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes slide-out {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(-20px) scale(0.95); }
}
</style>
<div class="icon"><slot name="icon"></slot></div>
<div class="content">
<slot></slot>
</div>
${dismissible ? `<button class="close-button" aria-label="Dismiss">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>` : ''}
`;
}
}
customElements.define('ds-toast', DsToast);