Files
dss/admin-ui/js/components/layout/ds-shell.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

756 lines
27 KiB
JavaScript

/**
* ds-shell.js
* Main shell component - provides IDE-style grid layout
* MVP2: Integrated with AdminStore and ProjectStore for settings and project management
*/
import './ds-activity-bar.js';
import './ds-panel.js';
import './ds-project-selector.js';
import './ds-ai-chat-sidebar.js';
import '../admin/ds-user-settings.js'; // Import settings component for direct instantiation
import '../ds-notification-center.js'; // Notification center component
import router from '../../core/router.js'; // Import Router for new architecture
import layoutManager from '../../core/layout-manager.js';
import toolBridge from '../../services/tool-bridge.js';
import contextStore from '../../stores/context-store.js';
import notificationService from '../../services/notification-service.js';
import { useAdminStore } from '../../stores/admin-store.js';
import { useProjectStore } from '../../stores/project-store.js';
import { useUserStore } from '../../stores/user-store.js';
import '../../config/component-registry.js'; // Ensure all panel components are loaded
import { authReady } from '../../utils/demo-auth-init.js'; // Auth initialization promise
class DSShell extends HTMLElement {
constructor() {
super();
this.currentTeam = 'ui'; // Default team
this.currentWorkdesk = null;
this.browserInitialized = false;
this.currentView = 'workdesk'; // Can be 'workdesk' or 'settings'
// MVP2: Initialize stores
this.adminStore = useAdminStore();
this.projectStore = useProjectStore();
this.userStore = useUserStore();
// Bind event handlers to avoid memory leaks
this.handleHashChangeBound = this.handleHashChange.bind(this);
}
async connectedCallback() {
// Render UI immediately (non-blocking)
this.render();
this.setupEventListeners();
// Initialize layout manager
layoutManager.init(this);
// Initialize Router (NEW - Phase 1 Architecture)
router.init();
// Wait for authentication to complete before making API calls
console.log('[DSShell] Waiting for authentication...');
const authResult = await authReady;
console.log('[DSShell] Authentication complete:', authResult);
// MVP2: Initialize store subscriptions (now safe to make API calls)
this.initializeStoreSubscriptions();
// Initialize notification service
notificationService.init();
// Set initial active link
this.updateActiveLink();
}
/**
* Cleanup when component is removed from DOM (prevents memory leaks)
*/
disconnectedCallback() {
// Remove event listener to prevent memory leak
window.removeEventListener('hashchange', this.handleHashChangeBound);
}
/**
* MVP2: Setup store subscriptions to keep context in sync
*/
initializeStoreSubscriptions() {
// Subscribe to admin settings changes
this.adminStore.subscribe(() => {
const settings = this.adminStore.getState();
contextStore.updateAdminSettings({
hostname: settings.hostname,
port: settings.port,
isRemote: settings.isRemote,
dssSetupType: settings.dssSetupType
});
console.log('[DSShell] Admin settings updated:', settings);
});
// Subscribe to project changes
this.projectStore.subscribe(() => {
const currentProject = this.projectStore.getCurrentProject();
if (currentProject) {
contextStore.setCurrentProject(currentProject);
console.log('[DSShell] Project context updated:', currentProject);
}
});
// Set initial project context
const currentProject = this.projectStore.getCurrentProject();
if (currentProject) {
contextStore.setCurrentProject(currentProject);
}
}
/**
* Initialize browser automation (required for DevTools components)
*/
async initializeBrowser() {
if (this.browserInitialized) {
console.log('[DSShell] Browser already initialized');
return true;
}
console.log('[DSShell] Browser init temporarily disabled - not critical for development');
this.browserInitialized = true; // Mark as initialized to skip
return true;
/* DISABLED - MCP browser tools not available yet
try {
await toolBridge.executeTool('browser_init', {
mode: 'remote',
url: window.location.origin
});
this.browserInitialized = true;
console.log('[DSShell] Browser automation initialized successfully');
return true;
} catch (error) {
console.error('[DSShell] Failed to initialize browser:', error);
this.browserInitialized = false;
return false;
}
*/
}
render() {
this.innerHTML = `
<ds-sidebar>
<div class="sidebar-header" style="display: flex; flex-direction: column; gap: 8px; padding-bottom: 12px; border-bottom: 1px solid var(--vscode-border);">
<div style="display: flex; align-items: center; gap: 8px;">
<span style="font-size: 24px; font-weight: 700;">⬡</span>
<div>
<div style="font-size: 13px; font-weight: 700; color: var(--vscode-text);">DSS</div>
<div style="font-size: 10px; color: var(--vscode-text-dim); line-height: 1.2;">Design System Studio</div>
</div>
</div>
</div>
<!-- NEW: Feature Module Navigation -->
<div class="sidebar-content">
<nav class="module-nav" style="display: flex; flex-direction: column; gap: 4px; padding-top: 12px;">
<a href="#projects" class="nav-item" data-path="projects" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">📁</span> Projects
</a>
<a href="#config" class="nav-item" data-path="config" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">⚙️</span> Configuration
</a>
<a href="#components" class="nav-item" data-path="components" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🧩</span> Components
</a>
<a href="#translations" class="nav-item" data-path="translations" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🔄</span> Translations
</a>
<a href="#discovery" class="nav-item" data-path="discovery" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">🔍</span> Discovery
</a>
<div style="height: 1px; background: var(--vscode-border); margin: 8px 0;"></div>
<a href="#admin" class="nav-item" data-path="admin" style="display: flex; align-items: center; gap: 10px; padding: 8px 12px; color: var(--vscode-text-dim); text-decoration: none; border-radius: 4px; transition: all 0.1s; font-size: 13px;">
<span style="font-size: 16px;">👤</span> Admin
</a>
</nav>
</div>
</ds-sidebar>
<ds-stage>
<div class="stage-header" style="display: flex; justify-content: space-between; align-items: center; padding: 0 16px; border-bottom: 1px solid var(--vscode-border); background: var(--vscode-bg); min-height: 44px;">
<div class="stage-header-left" style="display: flex; align-items: center; gap: 12px;">
<!-- Hamburger Menu (Mobile) -->
<button id="hamburger-menu" class="hamburger-menu" style="display: none; padding: 6px 8px; background: transparent; border: none; color: var(--vscode-text-dim); cursor: pointer; font-size: 20px;" aria-label="Toggle sidebar">☰</button>
<!-- NEW: Project Selector -->
<ds-project-selector></ds-project-selector>
</div>
<div class="stage-header-right" id="stage-actions" style="display: flex; align-items: center; gap: 8px;">
<!-- Action buttons will be populated here -->
</div>
</div>
<div class="stage-content">
<div id="stage-workdesk-content" style="height: 100%; overflow: auto;">
<!-- Dynamic Module Content via Router -->
</div>
</div>
</ds-stage>
<ds-ai-chat-sidebar></ds-ai-chat-sidebar>
`;
}
setupEventListeners() {
// Setup hamburger menu for mobile
this.setupMobileMenu();
// Setup navigation highlight for new module nav
this.setupNavigationHighlight();
// Populate stage-header-right with action buttons
const stageActions = this.querySelector('#stage-actions');
if (stageActions && stageActions.children.length === 0) {
stageActions.innerHTML = `
<button id="chat-toggle-btn" aria-label="Toggle AI Chat sidebar" aria-pressed="false" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Toggle Chat (💬)">💬</button>
<button id="advanced-mode-btn" aria-label="Toggle Advanced Mode" aria-pressed="false" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Advanced Mode (🔧)">🔧</button>
<div style="position: relative;">
<button id="notification-toggle-btn" aria-label="Notifications" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
position: relative;
" title="Notifications (🔔)">🔔
<span id="notification-indicator" style="
position: absolute;
top: 4px;
right: 4px;
width: 8px;
height: 8px;
background: var(--vscode-accent);
border-radius: 50%;
display: none;
"></span>
</button>
<ds-notification-center></ds-notification-center>
</div>
<button id="settings-btn" aria-label="Open Settings" style="
background: transparent;
border: none;
color: var(--vscode-text-dim);
cursor: pointer;
padding: 6px 8px;
font-size: 16px;
border-radius: 4px;
transition: all 0.1s;
" title="Settings (⚙️)">⚙️</button>
`;
// Add event listeners to stage-header action buttons
const chatToggleBtn = this.querySelector('#chat-toggle-btn');
if (chatToggleBtn) {
chatToggleBtn.addEventListener('click', () => {
const chatSidebar = this.querySelector('ds-ai-chat-sidebar');
if (chatSidebar && chatSidebar.toggleCollapse) {
chatSidebar.toggleCollapse();
const pressed = chatSidebar.isCollapsed ? 'false' : 'true';
chatToggleBtn.setAttribute('aria-pressed', pressed);
}
});
chatToggleBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
chatToggleBtn.click();
} else if (e.key === 'Escape') {
const chatSidebar = this.querySelector('ds-ai-chat-sidebar');
if (chatSidebar && !chatSidebar.isCollapsed) {
chatToggleBtn.click();
}
}
});
chatToggleBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
chatToggleBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
const advancedModeBtn = this.querySelector('#advanced-mode-btn');
if (advancedModeBtn) {
advancedModeBtn.addEventListener('click', () => {
this.toggleAdvancedMode();
});
advancedModeBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
advancedModeBtn.click();
}
});
advancedModeBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
advancedModeBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
const settingsBtn = this.querySelector('#settings-btn');
if (settingsBtn) {
settingsBtn.addEventListener('click', () => {
this.openSettings();
});
settingsBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
settingsBtn.click();
}
});
settingsBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
settingsBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
// Notification Center integration
const notificationToggleBtn = this.querySelector('#notification-toggle-btn');
const notificationCenter = this.querySelector('ds-notification-center');
const notificationIndicator = this.querySelector('#notification-indicator');
if (notificationToggleBtn && notificationCenter) {
// Toggle notification panel
notificationToggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = notificationCenter.hasAttribute('open');
if (isOpen) {
notificationCenter.removeAttribute('open');
} else {
notificationCenter.setAttribute('open', '');
}
});
// Close when clicking outside
document.addEventListener('click', (e) => {
if (!notificationCenter.contains(e.target) && !notificationToggleBtn.contains(e.target)) {
notificationCenter.removeAttribute('open');
}
});
// Update unread indicator
notificationService.addEventListener('unread-count-changed', (e) => {
const { count } = e.detail;
if (notificationIndicator) {
notificationIndicator.style.display = count > 0 ? 'block' : 'none';
}
});
// Handle notification actions (navigation)
notificationCenter.addEventListener('notification-action', (e) => {
const { event, payload } = e.detail;
console.log('[DSShell] Notification action:', event, payload);
// Handle navigation events
if (event.startsWith('navigate:')) {
const page = event.replace('navigate:', '');
// Route to the appropriate page
// This would integrate with your routing system
console.log('[DSShell] Navigate to:', page, payload);
}
});
// Hover effects
notificationToggleBtn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
notificationToggleBtn.addEventListener('mouseleave', (e) => {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
});
}
}
// Add team button event listeners
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach((btn, index) => {
btn.addEventListener('click', (e) => {
const teamId = e.target.dataset.team;
this.switchTeam(teamId);
});
// Keyboard navigation (Arrow keys)
btn.addEventListener('keydown', (e) => {
let nextBtn = null;
if (e.key === 'ArrowRight') {
e.preventDefault();
nextBtn = teamBtns[(index + 1) % teamBtns.length];
} else if (e.key === 'ArrowLeft') {
e.preventDefault();
nextBtn = teamBtns[(index - 1 + teamBtns.length) % teamBtns.length];
}
if (nextBtn) {
nextBtn.focus();
nextBtn.click();
}
});
// Hover effects
btn.addEventListener('mouseenter', (e) => {
e.target.style.color = 'var(--vscode-text)';
e.target.style.background = 'var(--vscode-selection)';
});
btn.addEventListener('mouseleave', (e) => {
// Keep accent color if this is the active team
if (e.target.classList.contains('active')) {
e.target.style.color = 'var(--vscode-accent)';
e.target.style.background = 'var(--vscode-selection)';
} else {
e.target.style.color = 'var(--vscode-text-dim)';
e.target.style.background = 'transparent';
}
});
});
// Set initial active team button
this.updateTeamButtonStates();
}
updateTeamButtonStates() {
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach(btn => {
if (btn.dataset.team === this.currentTeam) {
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
btn.style.color = 'var(--vscode-accent)';
btn.style.background = 'var(--vscode-selection)';
btn.style.borderColor = 'var(--vscode-accent)';
} else {
btn.classList.remove('active');
btn.setAttribute('aria-selected', 'false');
btn.style.color = 'var(--vscode-text-dim)';
btn.style.background = 'transparent';
btn.style.borderColor = 'transparent';
}
});
}
setupMobileMenu() {
const hamburgerBtn = this.querySelector('#hamburger-menu');
const sidebar = this.querySelector('ds-sidebar');
if (hamburgerBtn) {
hamburgerBtn.addEventListener('click', () => {
if (sidebar) {
const isOpen = sidebar.classList.contains('mobile-open');
if (isOpen) {
sidebar.classList.remove('mobile-open');
hamburgerBtn.setAttribute('aria-expanded', 'false');
} else {
sidebar.classList.add('mobile-open');
hamburgerBtn.setAttribute('aria-expanded', 'true');
}
}
});
hamburgerBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
hamburgerBtn.click();
}
});
}
// Close sidebar when clicking on a team button (mobile)
const teamBtns = this.querySelectorAll('.team-btn');
teamBtns.forEach(btn => {
btn.addEventListener('click', () => {
if (sidebar && window.innerWidth <= 768) {
sidebar.classList.remove('mobile-open');
if (hamburgerBtn) {
hamburgerBtn.setAttribute('aria-expanded', 'false');
}
}
});
});
// Show/hide hamburger menu based on screen size
const updateMenuVisibility = () => {
if (hamburgerBtn) {
if (window.innerWidth <= 768) {
hamburgerBtn.style.display = 'flex';
} else {
hamburgerBtn.style.display = 'none';
if (sidebar) {
sidebar.classList.remove('mobile-open');
}
}
}
};
updateMenuVisibility();
window.addEventListener('resize', updateMenuVisibility);
}
toggleAdvancedMode() {
// Get activity bar for advanced mode state (or create local tracking)
const activityBar = this.querySelector('ds-activity-bar');
let advancedMode = false;
if (activityBar && activityBar.advancedMode !== undefined) {
advancedMode = !activityBar.advancedMode;
activityBar.advancedMode = advancedMode;
activityBar.saveAdvancedMode();
} else {
// Fallback: use localStorage directly
advancedMode = localStorage.getItem('dss-advanced-mode') !== 'true';
localStorage.setItem('dss-advanced-mode', advancedMode.toString());
}
this.onAdvancedModeChange(advancedMode);
// Update button appearance and accessibility state
const advancedModeBtn = this.querySelector('#advanced-mode-btn');
if (advancedModeBtn) {
advancedModeBtn.setAttribute('aria-pressed', advancedMode.toString());
advancedModeBtn.style.color = advancedMode ? 'var(--vscode-accent)' : 'var(--vscode-text-dim)';
}
}
onAdvancedModeChange(advancedMode) {
console.log(`Advanced mode: ${advancedMode ? 'ON' : 'OFF'}`);
// Reconfigure panel with new advanced mode setting
const panel = this.querySelector('ds-panel');
if (panel) {
panel.configure(this.currentTeam, advancedMode);
}
}
async switchTeam(teamId) {
console.log(`Switching to team: ${teamId}`);
this.currentTeam = teamId;
// Persist team selection to userStore
this.userStore.updatePreferences({ lastTeam: teamId });
// Update team button states
this.updateTeamButtonStates();
// Update stage title
const stageTitle = this.querySelector('#stage-title');
if (stageTitle) {
stageTitle.textContent = `${teamId.toUpperCase()} Workdesk`;
}
// Apply admin-mode class for full-page layout
if (teamId === 'admin') {
this.classList.add('admin-mode');
// Initialize browser automation for admin team (needed for DevTools components)
this.initializeBrowser().catch(error => {
console.warn('[DSShell] Browser initialization failed (non-blocking):', error.message);
});
} else {
this.classList.remove('admin-mode');
}
// Configure panel for this team
const panel = this.querySelector('ds-panel');
const activityBar = this.querySelector('ds-activity-bar');
if (panel) {
// Get advancedMode from activity bar
const advancedMode = activityBar?.advancedMode || false;
panel.configure(teamId, advancedMode);
}
// Use layout manager to switch workdesk
try {
this.currentWorkdesk = await layoutManager.switchWorkdesk(teamId);
} catch (error) {
console.error(`Failed to load workdesk for team ${teamId}:`, error);
// Show error in stage
const stageContent = this.querySelector('#stage-workdesk-content');
if (stageContent) {
stageContent.innerHTML = `
<div style="text-align: center; padding: 48px; color: #f48771;">
<h2>Failed to load ${teamId.toUpperCase()} Workdesk</h2>
<p style="margin-top: 16px;">Error: ${error.message}</p>
</div>
`;
}
}
}
/**
* Open user settings view
*/
async openSettings() {
this.currentView = 'settings';
const stageContent = this.querySelector('#stage-workdesk-content');
const stageTitle = this.querySelector('#stage-title');
if (stageTitle) {
stageTitle.textContent = '⚙️ Settings';
}
if (stageContent) {
// Clear existing content
stageContent.innerHTML = '';
// Create and append user settings component
const settingsComponent = document.createElement('ds-user-settings');
stageContent.appendChild(settingsComponent);
}
// Hide sidebar and minimize panel for full-width settings
const sidebar = this.querySelector('ds-sidebar');
const panel = this.querySelector('ds-panel');
if (sidebar) {
sidebar.classList.add('collapsed');
}
if (panel) {
panel.classList.add('collapsed');
}
console.log('[DSShell] Settings view opened');
}
/**
* Close settings view and return to workdesk
*/
closeSettings() {
if (this.currentView === 'settings') {
this.currentView = 'workdesk';
// Restore sidebar and panel
const sidebar = this.querySelector('ds-sidebar');
const panel = this.querySelector('ds-panel');
if (sidebar) {
sidebar.classList.remove('collapsed');
}
if (panel) {
panel.classList.remove('collapsed');
}
// Reload current team's workdesk
this.switchTeam(this.currentTeam);
}
}
setupNavigationHighlight() {
// Use requestAnimationFrame to ensure DOM is ready (fixes race condition)
requestAnimationFrame(() => {
const navItems = this.querySelectorAll('.nav-item');
if (navItems.length === 0) {
console.warn('[DSShell] No nav items found for highlight setup');
return;
}
navItems.forEach(item => {
item.addEventListener('mouseenter', (e) => {
if (!e.target.classList.contains('active')) {
e.target.style.background = 'var(--vscode-list-hoverBackground, rgba(255,255,255,0.1))';
e.target.style.color = 'var(--vscode-text)';
}
});
item.addEventListener('mouseleave', (e) => {
if (!e.target.classList.contains('active')) {
e.target.style.background = 'transparent';
e.target.style.color = 'var(--vscode-text-dim)';
}
});
});
// Use bound handler to enable proper cleanup (fixes memory leak)
window.addEventListener('hashchange', this.handleHashChangeBound);
});
}
/**
* Handle hash change events (bound in constructor for proper cleanup)
*/
handleHashChange() {
this.updateActiveLink();
}
updateActiveLink(path) {
const currentPath = path || (window.location.hash.replace('#', '') || 'projects');
const navItems = this.querySelectorAll('.nav-item');
navItems.forEach(item => {
const itemPath = item.dataset.path;
if (itemPath === currentPath) {
item.classList.add('active');
item.style.background = 'var(--vscode-list-activeSelectionBackground, var(--vscode-selection))';
item.style.color = 'var(--vscode-list-activeSelectionForeground, var(--vscode-accent))';
item.style.fontWeight = '500';
} else {
item.classList.remove('active');
item.style.background = 'transparent';
item.style.color = 'var(--vscode-text-dim)';
item.style.fontWeight = 'normal';
}
});
}
// Getters for workdesk components to access
get sidebarContent() {
return this.querySelector('#sidebar-workdesk-content');
}
get stageContent() {
return this.querySelector('#stage-workdesk-content');
}
get stageActions() {
return this.querySelector('#stage-actions');
}
}
// Define custom element
customElements.define('ds-shell', DSShell);
// Also define the sidebar and stage as custom elements for CSS targeting
class DSSidebar extends HTMLElement {}
class DSStage extends HTMLElement {}
customElements.define('ds-sidebar', DSSidebar);
customElements.define('ds-stage', DSStage);