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
This commit is contained in:
132
admin-ui/js/components/layout/ds-activity-bar.js
Normal file
132
admin-ui/js/components/layout/ds-activity-bar.js
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* ds-activity-bar.js
|
||||
* Activity bar component - team/project switcher
|
||||
*/
|
||||
|
||||
class DSActivityBar extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentTeam = 'ui';
|
||||
this.advancedMode = this.loadAdvancedMode();
|
||||
this.teams = [
|
||||
{ id: 'ui', label: 'UI', icon: '🎨' },
|
||||
{ id: 'ux', label: 'UX', icon: '👁️' },
|
||||
{ id: 'qa', label: 'QA', icon: '🔍' },
|
||||
{ id: 'admin', label: 'Admin', icon: '🛡️' }
|
||||
];
|
||||
}
|
||||
|
||||
loadAdvancedMode() {
|
||||
try {
|
||||
return localStorage.getItem('dss-advanced-mode') === 'true';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
saveAdvancedMode() {
|
||||
try {
|
||||
localStorage.setItem('dss-advanced-mode', this.advancedMode.toString());
|
||||
} catch (e) {
|
||||
console.error('Failed to save advanced mode preference:', e);
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
${this.teams.map(team => `
|
||||
<div class="activity-item ${team.id === this.currentTeam ? 'active' : ''}"
|
||||
data-team="${team.id}"
|
||||
title="${team.label} Team">
|
||||
<span style="font-size: 20px;">${team.icon}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
|
||||
<div style="flex: 1;"></div>
|
||||
|
||||
<div class="activity-item"
|
||||
data-action="chat"
|
||||
title="AI Chat">
|
||||
<span style="font-size: 18px;">💬</span>
|
||||
</div>
|
||||
|
||||
<div class="activity-item ${this.advancedMode ? 'active' : ''}"
|
||||
data-action="advanced-mode"
|
||||
title="Advanced Mode: ${this.advancedMode ? 'ON' : 'OFF'}">
|
||||
<span style="font-size: 18px;">🔧</span>
|
||||
</div>
|
||||
|
||||
<div class="activity-item"
|
||||
data-action="settings"
|
||||
title="Settings">
|
||||
<span style="font-size: 18px;">⚙️</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.querySelectorAll('.activity-item[data-team]').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
const teamId = e.currentTarget.dataset.team;
|
||||
this.switchTeam(teamId);
|
||||
});
|
||||
});
|
||||
|
||||
this.querySelector('.activity-item[data-action="chat"]')?.addEventListener('click', () => {
|
||||
// Toggle chat sidebar visibility
|
||||
const chatSidebar = document.querySelector('ds-ai-chat-sidebar');
|
||||
if (chatSidebar && chatSidebar.toggleCollapse) {
|
||||
chatSidebar.toggleCollapse();
|
||||
}
|
||||
});
|
||||
|
||||
this.querySelector('.activity-item[data-action="advanced-mode"]')?.addEventListener('click', () => {
|
||||
this.toggleAdvancedMode();
|
||||
});
|
||||
|
||||
this.querySelector('.activity-item[data-action="settings"]')?.addEventListener('click', () => {
|
||||
// Dispatch settings-open event to parent shell
|
||||
this.dispatchEvent(new CustomEvent('settings-open', {
|
||||
bubbles: true,
|
||||
detail: { action: 'open-settings' }
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
toggleAdvancedMode() {
|
||||
this.advancedMode = !this.advancedMode;
|
||||
this.saveAdvancedMode();
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
|
||||
// Dispatch advanced-mode-change event to parent shell
|
||||
this.dispatchEvent(new CustomEvent('advanced-mode-change', {
|
||||
bubbles: true,
|
||||
detail: { advancedMode: this.advancedMode }
|
||||
}));
|
||||
}
|
||||
|
||||
switchTeam(teamId) {
|
||||
if (teamId === this.currentTeam) return;
|
||||
|
||||
this.currentTeam = teamId;
|
||||
|
||||
// Update active state
|
||||
this.querySelectorAll('.activity-item[data-team]').forEach(item => {
|
||||
item.classList.toggle('active', item.dataset.team === teamId);
|
||||
});
|
||||
|
||||
// Dispatch team-switch event to parent shell
|
||||
this.dispatchEvent(new CustomEvent('team-switch', {
|
||||
bubbles: true,
|
||||
detail: { team: teamId }
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-activity-bar', DSActivityBar);
|
||||
269
admin-ui/js/components/layout/ds-ai-chat-sidebar.js
Normal file
269
admin-ui/js/components/layout/ds-ai-chat-sidebar.js
Normal file
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* ds-ai-chat-sidebar.js
|
||||
* AI Chat Sidebar wrapper component
|
||||
* Wraps ds-chat-panel with collapse/expand toggle and context binding
|
||||
* MVP2: Right sidebar integrated with 3-column layout
|
||||
*/
|
||||
|
||||
import contextStore from '../../stores/context-store.js';
|
||||
import { useUserStore } from '../../stores/user-store.js';
|
||||
|
||||
class DSAiChatSidebar extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.userStore = useUserStore();
|
||||
const preferences = this.userStore.getPreferences();
|
||||
this.isCollapsed = preferences.chatCollapsedState !== false; // Default to collapsed
|
||||
this.currentProject = null;
|
||||
this.currentTeam = null;
|
||||
this.currentPage = null;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
this.initializeContextSubscriptions();
|
||||
}
|
||||
|
||||
initializeContextSubscriptions() {
|
||||
// Subscribe to context changes to update chat panel context
|
||||
this.unsubscribe = contextStore.subscribe(({ state }) => {
|
||||
this.currentProject = state.project;
|
||||
this.currentTeam = state.team;
|
||||
this.currentPage = state.page;
|
||||
|
||||
// Update chat panel with current context
|
||||
const chatPanel = this.querySelector('ds-chat-panel');
|
||||
if (chatPanel && chatPanel.setContext) {
|
||||
chatPanel.setContext({
|
||||
project: this.currentProject,
|
||||
team: this.currentTeam,
|
||||
page: this.currentPage
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Get initial context
|
||||
const context = contextStore.getState();
|
||||
if (context) {
|
||||
this.currentProject = context.currentProject || context.project || null;
|
||||
this.currentTeam = context.teamId || context.team || null;
|
||||
this.currentPage = context.page || null;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const buttonClass = this.isCollapsed ? 'rotating' : '';
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="ai-chat-container" style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: var(--vscode-sidebar-background);
|
||||
border-left: 1px solid var(--vscode-border);
|
||||
" role="complementary" aria-label="AI Assistant sidebar">
|
||||
<!-- Header with animated collapse button (shown when expanded) -->
|
||||
<div style="
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--vscode-bg);
|
||||
${this.isCollapsed ? 'display: none;' : ''}
|
||||
">
|
||||
<div style="
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
color: var(--vscode-foreground);
|
||||
">💬 AI Assistant</div>
|
||||
<button
|
||||
id="toggle-collapse-btn"
|
||||
class="ai-chat-toggle-btn ${buttonClass}"
|
||||
aria-label="Toggle chat sidebar"
|
||||
aria-expanded="${!this.isCollapsed}"
|
||||
style="
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 4px 8px;
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
title="Toggle Chat Sidebar">
|
||||
◀
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Chat content (collapsible) -->
|
||||
<div class="chat-content" style="
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
${this.isCollapsed ? 'display: none;' : ''}
|
||||
">
|
||||
<!-- Chat panel will be hydrated here via component registry -->
|
||||
<div id="chat-panel-container" style="
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
"></div>
|
||||
</div>
|
||||
|
||||
<!-- Collapsed state indicator (shown when collapsed) -->
|
||||
${this.isCollapsed ? `
|
||||
<button
|
||||
id="toggle-collapse-btn-collapsed"
|
||||
class="ai-chat-toggle-btn ${buttonClass}"
|
||||
aria-label="Expand chat sidebar"
|
||||
aria-expanded="false"
|
||||
style="
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-foreground);
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"
|
||||
title="Expand Chat Sidebar">
|
||||
💬
|
||||
</button>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async setupEventListeners() {
|
||||
// Handle both expanded and collapsed toggle buttons
|
||||
const toggleBtn = this.querySelector('#toggle-collapse-btn');
|
||||
const toggleBtnCollapsed = this.querySelector('#toggle-collapse-btn-collapsed');
|
||||
|
||||
const attachToggleListener = (btn) => {
|
||||
if (btn) {
|
||||
btn.addEventListener('click', () => {
|
||||
this.toggleCollapse();
|
||||
});
|
||||
btn.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
btn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
attachToggleListener(toggleBtn);
|
||||
attachToggleListener(toggleBtnCollapsed);
|
||||
|
||||
// Hydrate chat panel on first connection
|
||||
const chatContainer = this.querySelector('#chat-panel-container');
|
||||
if (chatContainer && chatContainer.children.length === 0) {
|
||||
try {
|
||||
// Import component registry to load chat panel
|
||||
const { hydrateComponent } = await import('../../config/component-registry.js');
|
||||
await hydrateComponent('ds-chat-panel', chatContainer);
|
||||
console.log('[DSAiChatSidebar] Chat panel loaded');
|
||||
|
||||
// Set initial context on chat panel
|
||||
const chatPanel = chatContainer.querySelector('ds-chat-panel');
|
||||
if (chatPanel && chatPanel.setContext) {
|
||||
chatPanel.setContext({
|
||||
project: this.currentProject,
|
||||
team: this.currentTeam,
|
||||
page: this.currentPage
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DSAiChatSidebar] Failed to load chat panel:', error);
|
||||
chatContainer.innerHTML = `
|
||||
<div style="padding: 12px; color: var(--vscode-error); font-size: 12px;">
|
||||
Failed to load chat panel
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleCollapse() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
|
||||
// Persist chat collapsed state to userStore
|
||||
this.userStore.updatePreferences({ chatCollapsedState: this.isCollapsed });
|
||||
|
||||
// Update CSS class for smooth CSS transition (avoid re-render for better UX)
|
||||
if (this.isCollapsed) {
|
||||
this.classList.add('collapsed');
|
||||
} else {
|
||||
this.classList.remove('collapsed');
|
||||
}
|
||||
|
||||
// Update button classes for rotation animation
|
||||
const btns = this.querySelectorAll('.ai-chat-toggle-btn');
|
||||
btns.forEach(btn => {
|
||||
if (this.isCollapsed) {
|
||||
btn.classList.add('rotating');
|
||||
} else {
|
||||
btn.classList.remove('rotating');
|
||||
}
|
||||
btn.setAttribute('aria-expanded', String(!this.isCollapsed));
|
||||
});
|
||||
|
||||
// Update header and content visibility with inline styles
|
||||
const header = this.querySelector('[style*="padding: 12px"]');
|
||||
const content = this.querySelector('.chat-content');
|
||||
|
||||
if (header) {
|
||||
if (this.isCollapsed) {
|
||||
header.style.display = 'none';
|
||||
} else {
|
||||
header.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
if (content) {
|
||||
if (this.isCollapsed) {
|
||||
content.style.display = 'none';
|
||||
} else {
|
||||
content.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle collapsed button visibility
|
||||
let collapsedBtn = this.querySelector('#toggle-collapse-btn-collapsed');
|
||||
if (!collapsedBtn && this.isCollapsed) {
|
||||
// Create the collapsed button if needed
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
} else if (collapsedBtn && !this.isCollapsed) {
|
||||
// Remove the collapsed button if needed
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
// Dispatch event for layout adjustment
|
||||
this.dispatchEvent(new CustomEvent('chat-sidebar-toggled', {
|
||||
detail: { isCollapsed: this.isCollapsed },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-ai-chat-sidebar', DSAiChatSidebar);
|
||||
120
admin-ui/js/components/layout/ds-panel.js
Normal file
120
admin-ui/js/components/layout/ds-panel.js
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* ds-panel.js
|
||||
* Bottom panel component - holds team-specific tabs
|
||||
*/
|
||||
|
||||
import { getPanelConfig } from '../../config/panel-config.js';
|
||||
|
||||
class DSPanel extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.currentTab = null;
|
||||
this.tabs = [];
|
||||
this.advancedMode = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure panel with team-specific tabs
|
||||
* @param {string} teamId - Team identifier (ui, ux, qa, admin)
|
||||
* @param {boolean} advancedMode - Whether advanced mode is enabled
|
||||
*/
|
||||
configure(teamId, advancedMode = false) {
|
||||
this.advancedMode = advancedMode;
|
||||
this.tabs = getPanelConfig(teamId, advancedMode);
|
||||
|
||||
// Set first tab as current if not already set
|
||||
if (this.tabs.length > 0 && !this.currentTab) {
|
||||
this.currentTab = this.tabs[0].id;
|
||||
}
|
||||
|
||||
// Re-render with new configuration
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.render();
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div class="panel-header">
|
||||
${this.tabs.map(tab => `
|
||||
<div class="panel-tab ${tab.id === this.currentTab ? 'active' : ''}"
|
||||
data-tab="${tab.id}">
|
||||
${tab.label}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div id="panel-tab-content">
|
||||
${this.renderTabContent(this.currentTab)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
this.querySelectorAll('.panel-tab').forEach(tab => {
|
||||
tab.addEventListener('click', (e) => {
|
||||
const tabId = e.currentTarget.dataset.tab;
|
||||
this.switchTab(tabId);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
switchTab(tabId) {
|
||||
if (tabId === this.currentTab) return;
|
||||
|
||||
this.currentTab = tabId;
|
||||
|
||||
// Update active state
|
||||
this.querySelectorAll('.panel-tab').forEach(tab => {
|
||||
tab.classList.toggle('active', tab.dataset.tab === tabId);
|
||||
});
|
||||
|
||||
// Update content
|
||||
const content = this.querySelector('#panel-tab-content');
|
||||
if (content) {
|
||||
content.innerHTML = this.renderTabContent(tabId);
|
||||
}
|
||||
|
||||
// Dispatch tab-switch event
|
||||
this.dispatchEvent(new CustomEvent('panel-tab-switch', {
|
||||
bubbles: true,
|
||||
detail: { tab: tabId }
|
||||
}));
|
||||
}
|
||||
|
||||
renderTabContent(tabId) {
|
||||
// Find tab configuration
|
||||
const tabConfig = this.tabs.find(tab => tab.id === tabId);
|
||||
if (!tabConfig) {
|
||||
return '<div style="padding: 16px; color: var(--vscode-text-dim);">Tab not found</div>';
|
||||
}
|
||||
|
||||
// Dynamically create component based on configuration
|
||||
const componentTag = tabConfig.component;
|
||||
const propsString = Object.entries(tabConfig.props || {})
|
||||
.map(([key, value]) => `${key}="${value}"`)
|
||||
.join(' ');
|
||||
|
||||
return `<${componentTag} ${propsString}></${componentTag}>`;
|
||||
}
|
||||
|
||||
// Public method for workdesks to update panel content
|
||||
setTabContent(tabId, content) {
|
||||
const tabContent = this.querySelector('#panel-tab-content');
|
||||
if (this.currentTab === tabId && tabContent) {
|
||||
if (typeof content === 'string') {
|
||||
tabContent.innerHTML = content;
|
||||
} else {
|
||||
tabContent.innerHTML = '';
|
||||
tabContent.appendChild(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-panel', DSPanel);
|
||||
380
admin-ui/js/components/layout/ds-project-selector.js
Normal file
380
admin-ui/js/components/layout/ds-project-selector.js
Normal file
@@ -0,0 +1,380 @@
|
||||
/**
|
||||
* ds-project-selector.js
|
||||
* Project selector component for workdesk header
|
||||
* MVP1: Enforces project selection before tools can be used
|
||||
* FIXED: Now uses authenticated apiClient instead of direct fetch()
|
||||
*/
|
||||
|
||||
import contextStore from '../../stores/context-store.js';
|
||||
import apiClient from '../../services/api-client.js';
|
||||
import { ComponentHelpers } from '../../utils/component-helpers.js';
|
||||
|
||||
class DSProjectSelector extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.projects = [];
|
||||
this.isLoading = false;
|
||||
this.selectedProject = contextStore.get('projectId');
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.render();
|
||||
await this.loadProjects();
|
||||
this.setupEventListeners();
|
||||
|
||||
// Subscribe to context changes
|
||||
this.unsubscribe = contextStore.subscribeToKey('projectId', (newValue) => {
|
||||
this.selectedProject = newValue;
|
||||
this.updateSelectedDisplay();
|
||||
});
|
||||
|
||||
// Bind auth change handler to this component
|
||||
this.handleAuthChange = async (event) => {
|
||||
console.log('[DSProjectSelector] Auth state changed, reloading projects');
|
||||
await this.reloadProjects();
|
||||
};
|
||||
|
||||
// Listen for custom auth-change events (fires when tokens are refreshed)
|
||||
document.addEventListener('auth-change', this.handleAuthChange);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
if (this.unsubscribe) {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
// Clean up auth change listener
|
||||
if (this.handleAuthChange) {
|
||||
document.removeEventListener('auth-change', this.handleAuthChange);
|
||||
}
|
||||
|
||||
// Clean up document click listener for closing dropdown
|
||||
if (this.closeDropdownHandler) {
|
||||
document.removeEventListener('click', this.closeDropdownHandler);
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjects() {
|
||||
this.isLoading = true;
|
||||
this.updateLoadingState();
|
||||
|
||||
try {
|
||||
// Fetch projects from authenticated API client
|
||||
// This ensures Authorization header is sent with the request
|
||||
this.projects = await apiClient.getProjects();
|
||||
|
||||
console.log(`[DSProjectSelector] Loaded ${this.projects.length} projects`);
|
||||
|
||||
// If no project selected but we have projects, show prompt
|
||||
if (!this.selectedProject && this.projects.length > 0) {
|
||||
this.showProjectModal();
|
||||
}
|
||||
|
||||
this.renderDropdown();
|
||||
} catch (error) {
|
||||
console.error('[DSProjectSelector] Failed to load projects:', error);
|
||||
|
||||
// Fallback: Create mock admin-ui project for development
|
||||
this.projects = [{
|
||||
id: 'admin-ui',
|
||||
name: 'Admin UI (Default)',
|
||||
description: 'Design System Server Admin UI'
|
||||
}];
|
||||
|
||||
// Auto-select if no project selected
|
||||
if (!this.selectedProject) {
|
||||
try {
|
||||
contextStore.setProject('admin-ui');
|
||||
this.selectedProject = 'admin-ui';
|
||||
} catch (storeError) {
|
||||
console.error('[DSProjectSelector] Error setting project:', storeError);
|
||||
this.selectedProject = 'admin-ui';
|
||||
}
|
||||
}
|
||||
|
||||
this.renderDropdown();
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
this.updateLoadingState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Public method to reload projects - called when auth state changes
|
||||
*/
|
||||
async reloadProjects() {
|
||||
console.log('[DSProjectSelector] Reloading projects due to auth state change');
|
||||
await this.loadProjects();
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
const button = this.querySelector('#project-selector-button');
|
||||
const dropdown = this.querySelector('#project-dropdown');
|
||||
|
||||
if (button && dropdown) {
|
||||
// Add click listener to button (delegation handles via event target check)
|
||||
button.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
|
||||
});
|
||||
}
|
||||
|
||||
// Add click listeners to dropdown items
|
||||
const projectOptions = this.querySelectorAll('.project-option');
|
||||
projectOptions.forEach(option => {
|
||||
option.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const projectId = option.dataset.projectId;
|
||||
this.selectProject(projectId);
|
||||
});
|
||||
});
|
||||
|
||||
// Close dropdown when clicking outside - stored for cleanup
|
||||
if (!this.closeDropdownHandler) {
|
||||
this.closeDropdownHandler = (e) => {
|
||||
if (!this.contains(e.target) && dropdown) {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
};
|
||||
document.addEventListener('click', this.closeDropdownHandler);
|
||||
}
|
||||
}
|
||||
|
||||
selectProject(projectId) {
|
||||
const project = this.projects.find(p => p.id === projectId);
|
||||
if (!project) {
|
||||
console.error('[DSProjectSelector] Project not found:', projectId);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
contextStore.setProject(projectId);
|
||||
this.selectedProject = projectId;
|
||||
|
||||
// Close dropdown
|
||||
const dropdown = this.querySelector('#project-dropdown');
|
||||
if (dropdown) {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
|
||||
this.updateSelectedDisplay();
|
||||
|
||||
ComponentHelpers.showToast?.(`Switched to project: ${project.name}`, 'success');
|
||||
|
||||
// Notify other components of project change
|
||||
this.dispatchEvent(new CustomEvent('project-changed', {
|
||||
detail: { projectId },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error('[DSProjectSelector] Error selecting project:', error);
|
||||
ComponentHelpers.showToast?.(`Failed to select project: ${error.message}`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showProjectModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'project-selection-modal';
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
`;
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.style.cssText = `
|
||||
background: var(--vscode-sidebar);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
padding: 24px;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||
`;
|
||||
|
||||
// Use event delegation instead of attaching listeners to individual buttons
|
||||
content.innerHTML = `
|
||||
<h2 style="font-size: 16px; margin-bottom: 12px;">Select a Project</h2>
|
||||
<p style="font-size: 12px; color: var(--vscode-text-dim); margin-bottom: 16px;">
|
||||
Please select a project to start working. All tools require an active project.
|
||||
</p>
|
||||
<div style="display: flex; flex-direction: column; gap: 8px;" id="project-buttons-container">
|
||||
${this.projects.map(project => `
|
||||
<button
|
||||
class="project-modal-button"
|
||||
data-project-id="${project.id}"
|
||||
type="button"
|
||||
style="padding: 12px; background: var(--vscode-bg); border: 1px solid var(--vscode-border); border-radius: 4px; cursor: pointer; text-align: left; font-family: inherit; font-size: inherit;"
|
||||
>
|
||||
<div style="font-size: 12px; font-weight: 600;">${ComponentHelpers.escapeHtml(project.name)}</div>
|
||||
${project.description ? `<div style="font-size: 11px; color: var(--vscode-text-dim); margin-top: 4px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
|
||||
</button>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.appendChild(content);
|
||||
|
||||
// Store reference to component for event handlers
|
||||
const component = this;
|
||||
|
||||
// Use event delegation on content container
|
||||
const buttonContainer = content.querySelector('#project-buttons-container');
|
||||
if (buttonContainer) {
|
||||
buttonContainer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.project-modal-button');
|
||||
if (btn) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const projectId = btn.dataset.projectId;
|
||||
console.log('[DSProjectSelector] Modal button clicked:', projectId);
|
||||
try {
|
||||
component.selectProject(projectId);
|
||||
console.log('[DSProjectSelector] Project selected successfully');
|
||||
} catch (err) {
|
||||
console.error('[DSProjectSelector] Error selecting project:', err);
|
||||
} finally {
|
||||
// Ensure modal is always removed
|
||||
if (modal && modal.parentNode) {
|
||||
modal.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Close modal when clicking outside the content area
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
console.log('[DSProjectSelector] Closing modal (clicked outside)');
|
||||
modal.remove();
|
||||
}
|
||||
});
|
||||
|
||||
document.body.appendChild(modal);
|
||||
console.log('[DSProjectSelector] Project selection modal shown');
|
||||
}
|
||||
|
||||
updateSelectedDisplay() {
|
||||
const button = this.querySelector('#project-selector-button');
|
||||
if (!button) return;
|
||||
|
||||
const selectedProject = this.projects.find(p => p.id === this.selectedProject);
|
||||
if (selectedProject) {
|
||||
button.innerHTML = `
|
||||
<span style="font-size: 11px; color: var(--vscode-text-dim);">Project:</span>
|
||||
<span style="font-size: 12px; font-weight: 600; margin-left: 4px;">${ComponentHelpers.escapeHtml(selectedProject.name)}</span>
|
||||
<span style="margin-left: 6px;">▼</span>
|
||||
`;
|
||||
} else {
|
||||
button.innerHTML = `
|
||||
<span style="font-size: 12px; color: var(--vscode-text-dim);">Select Project</span>
|
||||
<span style="margin-left: 6px;">▼</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingState() {
|
||||
const button = this.querySelector('#project-selector-button');
|
||||
if (!button) return;
|
||||
|
||||
if (this.isLoading) {
|
||||
button.disabled = true;
|
||||
button.innerHTML = '<span style="font-size: 11px;">Loading projects...</span>';
|
||||
} else {
|
||||
button.disabled = false;
|
||||
this.updateSelectedDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
renderDropdown() {
|
||||
const dropdown = this.querySelector('#project-dropdown');
|
||||
if (!dropdown) return;
|
||||
|
||||
if (this.projects.length === 0) {
|
||||
dropdown.innerHTML = `
|
||||
<div style="padding: 12px; font-size: 11px; color: var(--vscode-text-dim);">
|
||||
No projects available
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
dropdown.innerHTML = `
|
||||
${this.projects.map(project => `
|
||||
<div
|
||||
class="project-option"
|
||||
data-project-id="${project.id}"
|
||||
style="
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--vscode-border);
|
||||
${this.selectedProject === project.id ? 'background: var(--vscode-list-activeSelectionBackground);' : ''}
|
||||
"
|
||||
>
|
||||
<div style="font-size: 12px; font-weight: 600;">
|
||||
${this.selectedProject === project.id ? '✓ ' : ''}${ComponentHelpers.escapeHtml(project.name)}
|
||||
</div>
|
||||
${project.description ? `<div style="font-size: 10px; color: var(--vscode-text-dim); margin-top: 2px;">${ComponentHelpers.escapeHtml(project.description)}</div>` : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
`;
|
||||
|
||||
// Re-attach event listeners to dropdown items
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.innerHTML = `
|
||||
<div style="position: relative; display: inline-block;">
|
||||
<button
|
||||
id="project-selector-button"
|
||||
style="
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
background: var(--vscode-sidebar);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: var(--vscode-text);
|
||||
"
|
||||
>
|
||||
<span style="font-size: 12px;">Loading...</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
id="project-dropdown"
|
||||
style="
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
margin-top: 4px;
|
||||
min-width: 250px;
|
||||
background: var(--vscode-sidebar);
|
||||
border: 1px solid var(--vscode-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
|
||||
z-index: 1000;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
"
|
||||
>
|
||||
<!-- Projects will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('ds-project-selector', DSProjectSelector);
|
||||
|
||||
export default DSProjectSelector;
|
||||
755
admin-ui/js/components/layout/ds-shell.js
Normal file
755
admin-ui/js/components/layout/ds-shell.js
Normal file
@@ -0,0 +1,755 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user