/** * 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 = `
`; } 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 = `
`; // 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 = `

Failed to load ${teamId.toUpperCase()} Workdesk

Error: ${error.message}

`; } } } /** * 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);