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:
Digital Production Factory
2025-12-09 18:45:48 -03:00
commit 276ed71f31
884 changed files with 373737 additions and 0 deletions

View 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);

View 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);

View 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);

View 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;

View 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);