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
435 lines
23 KiB
JavaScript
435 lines
23 KiB
JavaScript
/**
|
||
* ds-user-settings.js
|
||
* User settings page component
|
||
* Manages user profile, preferences, integrations, and account settings
|
||
* MVP3: Full integration with backend API and user-store
|
||
*/
|
||
|
||
import { useUserStore } from '../../stores/user-store.js';
|
||
|
||
export default class DSUserSettings extends HTMLElement {
|
||
constructor() {
|
||
super();
|
||
this.userStore = useUserStore();
|
||
this.activeTab = 'profile';
|
||
this.isLoading = false;
|
||
this.formChanges = {};
|
||
}
|
||
|
||
connectedCallback() {
|
||
this.render();
|
||
this.setupEventListeners();
|
||
this.subscribeToUserStore();
|
||
}
|
||
|
||
subscribeToUserStore() {
|
||
this.unsubscribe = this.userStore.subscribe(() => {
|
||
this.updateUI();
|
||
});
|
||
}
|
||
|
||
render() {
|
||
const user = this.userStore.getCurrentUser();
|
||
const displayName = this.userStore.getDisplayName();
|
||
const avatar = this.userStore.getAvatar();
|
||
|
||
this.innerHTML = `
|
||
<div style="display: flex; flex-direction: column; height: 100%; background: var(--vscode-bg);">
|
||
<!-- Header -->
|
||
<div style="padding: 24px; border-bottom: 1px solid var(--vscode-border);">
|
||
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 24px;">
|
||
<img src="${avatar}" alt="Avatar" style="width: 64px; height: 64px; border-radius: 8px; background: var(--vscode-sidebar);" />
|
||
<div>
|
||
<h1 style="margin: 0 0 4px 0; font-size: 24px;">${displayName}</h1>
|
||
<p style="margin: 0; color: var(--vscode-text-dim); font-size: 12px;">${user?.email || 'Not logged in'}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tabs -->
|
||
<div style="display: flex; border-bottom: 1px solid var(--vscode-border); padding: 0 24px; gap: 24px; flex-shrink: 0;">
|
||
<button class="settings-tab" data-tab="profile" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
|
||
👤 Profile
|
||
</button>
|
||
<button class="settings-tab" data-tab="preferences" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
|
||
⚙️ Preferences
|
||
</button>
|
||
<button class="settings-tab" data-tab="integrations" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
|
||
🔗 Integrations
|
||
</button>
|
||
<button class="settings-tab" data-tab="about" style="padding: 12px 0; border: none; background: transparent; color: var(--vscode-text-dim); cursor: pointer; border-bottom: 2px solid transparent; font-size: 13px; transition: all 0.2s;">
|
||
ℹ️ About
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Content -->
|
||
<div style="flex: 1; overflow-y: auto; padding: 24px;">
|
||
<!-- Profile Tab -->
|
||
<div id="profile-tab" class="settings-content">
|
||
<div style="max-width: 600px;">
|
||
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Profile Settings</h2>
|
||
|
||
<div style="margin-bottom: 16px;">
|
||
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Full Name</label>
|
||
<input id="profile-name" type="text" value="${user?.name || ''}" placeholder="Your full name" style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px;" />
|
||
</div>
|
||
|
||
<div style="margin-bottom: 16px;">
|
||
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Email</label>
|
||
<input id="profile-email" type="email" value="${user?.email || ''}" placeholder="your@email.com" style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px;" />
|
||
</div>
|
||
|
||
<div style="margin-bottom: 16px;">
|
||
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Role</label>
|
||
<div style="padding: 8px 12px; background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); color: var(--vscode-text-dim); border-radius: 4px; font-size: 13px;">
|
||
${user?.role || 'User'} <span style="color: var(--vscode-text-dim); font-size: 11px;">(Read-only)</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-bottom: 24px;">
|
||
<label style="display: block; font-size: 12px; font-weight: 500; margin-bottom: 4px;">Bio</label>
|
||
<textarea id="profile-bio" placeholder="Tell us about yourself..." style="width: 100%; padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 13px; min-height: 80px; resize: vertical;" >${user?.bio || ''}</textarea>
|
||
</div>
|
||
|
||
<div style="display: flex; gap: 8px;">
|
||
<button id="save-profile-btn" style="padding: 8px 16px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
|
||
Save Changes
|
||
</button>
|
||
<button id="change-password-btn" style="padding: 8px 16px; background: var(--vscode-button-secondaryBackground); color: var(--vscode-button-secondaryForeground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
|
||
Change Password
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preferences Tab -->
|
||
<div id="preferences-tab" class="settings-content" style="display: none;">
|
||
<div style="max-width: 600px;">
|
||
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Preferences</h2>
|
||
|
||
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Theme</h3>
|
||
<div style="margin-bottom: 24px;">
|
||
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
|
||
<input type="radio" name="theme" value="dark" checked />
|
||
<span style="font-size: 12px;">Dark</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input type="radio" name="theme" value="light" />
|
||
<span style="font-size: 12px;">Light</span>
|
||
</label>
|
||
</div>
|
||
|
||
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Language</h3>
|
||
<div style="margin-bottom: 24px;">
|
||
<select id="pref-language" style="padding: 8px 12px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 4px; font-size: 12px; cursor: pointer;">
|
||
<option value="en">English</option>
|
||
<option value="es">Español</option>
|
||
<option value="fr">Français</option>
|
||
<option value="de">Deutsch</option>
|
||
<option value="ja">日本語</option>
|
||
</select>
|
||
</div>
|
||
|
||
<h3 style="margin: 0 0 12px 0; font-size: 14px; color: var(--vscode-text-dim);">Notifications</h3>
|
||
<div style="margin-bottom: 24px;">
|
||
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
|
||
<input id="pref-notifications" type="checkbox" checked />
|
||
<span style="font-size: 12px;">Enable notifications</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px; cursor: pointer;">
|
||
<input id="pref-email-notifications" type="checkbox" checked />
|
||
<span style="font-size: 12px;">Email notifications</span>
|
||
</label>
|
||
<label style="display: flex; align-items: center; gap: 8px; cursor: pointer;">
|
||
<input id="pref-desktop-notifications" type="checkbox" checked />
|
||
<span style="font-size: 12px;">Desktop notifications</span>
|
||
</label>
|
||
</div>
|
||
|
||
<button id="save-preferences-btn" style="padding: 8px 16px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
|
||
Save Preferences
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Integrations Tab -->
|
||
<div id="integrations-tab" class="settings-content" style="display: none;">
|
||
<div style="max-width: 600px;">
|
||
<h2 style="margin: 0 0 16px 0; font-size: 18px;">Integrations</h2>
|
||
|
||
<!-- Figma Integration -->
|
||
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<div>
|
||
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">🎨 Figma</h3>
|
||
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect your Figma account for design token extraction</p>
|
||
</div>
|
||
<span id="figma-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #4CAF50; color: white; border-radius: 3px; display: none;">Connected</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<input id="figma-api-key" type="password" placeholder="Enter Figma API key" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
|
||
<button class="integration-save-btn" data-service="figma" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- GitHub Integration -->
|
||
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<div>
|
||
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">🐙 GitHub</h3>
|
||
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect GitHub for component library integration</p>
|
||
</div>
|
||
<span id="github-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<input id="github-api-key" type="password" placeholder="Enter GitHub personal access token" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
|
||
<button class="integration-save-btn" data-service="github" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Jira Integration -->
|
||
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<div>
|
||
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">📋 Jira</h3>
|
||
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect Jira for issue tracking integration</p>
|
||
</div>
|
||
<span id="jira-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px; margin-bottom: 8px;">
|
||
<input id="jira-api-key" type="password" placeholder="Enter Jira API token" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
|
||
<button class="integration-save-btn" data-service="jira" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
|
||
</div>
|
||
<input id="jira-project-key" type="text" placeholder="Jira project key (optional)" style="width: 100%; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
|
||
</div>
|
||
|
||
<!-- Slack Integration -->
|
||
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 12px;">
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<div>
|
||
<h3 style="margin: 0 0 4px 0; font-size: 13px; font-weight: 500;">💬 Slack</h3>
|
||
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">Connect Slack for team notifications</p>
|
||
</div>
|
||
<span id="slack-status" class="integration-status" style="font-size: 11px; padding: 4px 8px; background: #666; color: white; border-radius: 3px; display: none;">Connected</span>
|
||
</div>
|
||
<div style="display: flex; gap: 8px;">
|
||
<input id="slack-webhook" type="password" placeholder="Enter Slack webhook URL" style="flex: 1; padding: 6px 10px; background: var(--vscode-input-background); border: 1px solid var(--vscode-border); color: var(--vscode-text); border-radius: 3px; font-size: 11px;" />
|
||
<button class="integration-save-btn" data-service="slack" style="padding: 6px 12px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); border: none; border-radius: 3px; cursor: pointer; font-size: 11px;">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- About Tab -->
|
||
<div id="about-tab" class="settings-content" style="display: none;">
|
||
<div style="max-width: 600px;">
|
||
<h2 style="margin: 0 0 16px 0; font-size: 18px;">About</h2>
|
||
|
||
<div style="background: var(--vscode-sidebar); border: 1px solid var(--vscode-border); border-radius: 4px; padding: 16px; margin-bottom: 16px;">
|
||
<h3 style="margin: 0 0 12px 0; font-size: 14px;">Design System Swarm</h3>
|
||
<p style="margin: 0 0 8px 0; font-size: 12px; color: var(--vscode-text-dim);">Version: 3.0.0 (MVP3)</p>
|
||
<p style="margin: 0 0 8px 0; font-size: 12px;">Advanced design system management platform with AI assistance, design tokens, and multi-team collaboration.</p>
|
||
<p style="margin: 0; font-size: 11px; color: var(--vscode-text-dim);">© 2024 Design System Swarm. All rights reserved.</p>
|
||
</div>
|
||
|
||
<h3 style="margin: 16px 0 8px 0; font-size: 14px;">Features</h3>
|
||
<ul style="margin: 0; padding-left: 20px; font-size: 12px;">
|
||
<li>Design token management and synchronization</li>
|
||
<li>Figma integration with automated token extraction</li>
|
||
<li>Multi-team collaboration workspace</li>
|
||
<li>AI-powered design system analysis</li>
|
||
<li>Storybook integration and component documentation</li>
|
||
<li>GitHub and Jira integration</li>
|
||
</ul>
|
||
|
||
<div style="margin-top: 24px;">
|
||
<h3 style="margin: 0 0 8px 0; font-size: 14px;">Account Actions</h3>
|
||
<button id="logout-btn" style="padding: 8px 16px; background: #F44336; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500;">
|
||
🚪 Logout
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
setupEventListeners() {
|
||
// Tab switching
|
||
this.querySelectorAll('.settings-tab').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
this.switchTab(e.target.closest('button').dataset.tab);
|
||
});
|
||
});
|
||
|
||
// Profile tab
|
||
const saveProfileBtn = this.querySelector('#save-profile-btn');
|
||
if (saveProfileBtn) {
|
||
saveProfileBtn.addEventListener('click', () => this.saveProfile());
|
||
}
|
||
|
||
// Preferences tab
|
||
const savePreferencesBtn = this.querySelector('#save-preferences-btn');
|
||
if (savePreferencesBtn) {
|
||
savePreferencesBtn.addEventListener('click', () => this.savePreferences());
|
||
}
|
||
|
||
// Integration save buttons
|
||
this.querySelectorAll('.integration-save-btn').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const service = e.target.dataset.service;
|
||
const apiKeyInput = this.querySelector(`#${service}-api-key`) || this.querySelector(`#${service}-webhook`);
|
||
const apiKey = apiKeyInput?.value || '';
|
||
this.saveIntegration(service, apiKey);
|
||
});
|
||
});
|
||
|
||
// Logout button
|
||
const logoutBtn = this.querySelector('#logout-btn');
|
||
if (logoutBtn) {
|
||
logoutBtn.addEventListener('click', () => this.logout());
|
||
}
|
||
|
||
// Change password button
|
||
const changePasswordBtn = this.querySelector('#change-password-btn');
|
||
if (changePasswordBtn) {
|
||
changePasswordBtn.addEventListener('click', () => this.showChangePasswordDialog());
|
||
}
|
||
}
|
||
|
||
switchTab(tabName) {
|
||
this.activeTab = tabName;
|
||
|
||
// Hide all tabs
|
||
this.querySelectorAll('.settings-content').forEach(tab => {
|
||
tab.style.display = 'none';
|
||
});
|
||
|
||
// Show selected tab
|
||
const selectedTab = this.querySelector(`#${tabName}-tab`);
|
||
if (selectedTab) {
|
||
selectedTab.style.display = 'block';
|
||
}
|
||
|
||
// Update tab styling
|
||
this.querySelectorAll('.settings-tab').forEach(btn => {
|
||
const isActive = btn.dataset.tab === tabName;
|
||
btn.style.borderBottomColor = isActive ? 'var(--vscode-accent)' : 'transparent';
|
||
btn.style.color = isActive ? 'var(--vscode-text)' : 'var(--vscode-text-dim)';
|
||
});
|
||
}
|
||
|
||
async saveProfile() {
|
||
const name = this.querySelector('#profile-name')?.value || '';
|
||
const email = this.querySelector('#profile-email')?.value || '';
|
||
const bio = this.querySelector('#profile-bio')?.value || '';
|
||
|
||
try {
|
||
await this.userStore.updateProfile({ name, email, bio });
|
||
this.showNotification('Profile saved successfully', 'success');
|
||
} catch (error) {
|
||
this.showNotification('Failed to save profile', 'error');
|
||
console.error(error);
|
||
}
|
||
}
|
||
|
||
savePreferences() {
|
||
const theme = this.querySelector('input[name="theme"]:checked')?.value || 'dark';
|
||
const language = this.querySelector('#pref-language')?.value || 'en';
|
||
const notifications = this.querySelector('#pref-notifications')?.checked || false;
|
||
|
||
this.userStore.updatePreferences({
|
||
theme,
|
||
language,
|
||
notifications: {
|
||
enabled: notifications,
|
||
email: this.querySelector('#pref-email-notifications')?.checked || false,
|
||
desktop: this.querySelector('#pref-desktop-notifications')?.checked || false
|
||
}
|
||
});
|
||
|
||
this.showNotification('Preferences saved', 'success');
|
||
}
|
||
|
||
saveIntegration(service, apiKey) {
|
||
if (!apiKey) {
|
||
this.userStore.removeIntegration(service);
|
||
this.showNotification(`${service} integration removed`, 'success');
|
||
} else {
|
||
const metadata = {};
|
||
if (service === 'jira') {
|
||
metadata.projectKey = this.querySelector('#jira-project-key')?.value || '';
|
||
}
|
||
this.userStore.setIntegration(service, apiKey, metadata);
|
||
this.showNotification(`${service} integration saved`, 'success');
|
||
}
|
||
|
||
this.updateIntegrationStatus();
|
||
}
|
||
|
||
updateIntegrationStatus() {
|
||
const integrations = this.userStore.getIntegrations();
|
||
['figma', 'github', 'jira', 'slack'].forEach(service => {
|
||
const status = this.querySelector(`#${service}-status`);
|
||
if (status) {
|
||
if (integrations[service]?.enabled) {
|
||
status.style.display = 'inline-block';
|
||
status.style.background = '#4CAF50';
|
||
status.textContent = 'Connected';
|
||
} else {
|
||
status.style.display = 'none';
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
updateUI() {
|
||
// Update display when user state changes
|
||
const user = this.userStore.getCurrentUser();
|
||
const displayName = this.userStore.getDisplayName();
|
||
|
||
// Re-render component
|
||
this.render();
|
||
this.setupEventListeners();
|
||
}
|
||
|
||
showChangePasswordDialog() {
|
||
// Placeholder for password change dialog
|
||
// In a real implementation, this would show a modal dialog
|
||
alert('Change password functionality would be implemented here.\n\nIn production, this would show a modal with current password and new password fields.');
|
||
}
|
||
|
||
showNotification(message, type = 'info') {
|
||
const notificationEl = document.createElement('div');
|
||
notificationEl.style.cssText = `
|
||
position: fixed;
|
||
bottom: 24px;
|
||
right: 24px;
|
||
padding: 12px 16px;
|
||
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#F44336' : '#0066CC'};
|
||
color: white;
|
||
border-radius: 4px;
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
z-index: 1000;
|
||
animation: slideInUp 0.3s ease-out;
|
||
`;
|
||
notificationEl.textContent = message;
|
||
document.body.appendChild(notificationEl);
|
||
|
||
setTimeout(() => {
|
||
notificationEl.style.animation = 'slideOutDown 0.3s ease-in';
|
||
setTimeout(() => notificationEl.remove(), 300);
|
||
}, 3000);
|
||
}
|
||
|
||
disconnectedCallback() {
|
||
if (this.unsubscribe) {
|
||
this.unsubscribe();
|
||
}
|
||
}
|
||
}
|
||
|
||
customElements.define('ds-user-settings', DSUserSettings);
|