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
168 lines
4.9 KiB
JavaScript
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);
|