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
1633 lines
46 KiB
Plaintext
1633 lines
46 KiB
Plaintext
<GENERATED-CODE>
|
|
1. Update `app-store.js` to add project management state and actions.
|
|
2. Create `ds-project-selector.js` to handle project switching in the UI.
|
|
3. Create the 6 module placeholder files for the new architecture.
|
|
4. Update `router.js` to map routes to these new modules.
|
|
5. Update `ds-shell.js` to implement the new sidebar navigation and layout.
|
|
|
|
<UPDATED_EXISTING_FILE: admin-ui/js/stores/app-store.js>
|
|
/**
|
|
* Design System Server (DSS) - App Store
|
|
*
|
|
* Centralized state management with reactive subscriptions
|
|
* for managing application state across components and pages.
|
|
*/
|
|
|
|
class AppStore {
|
|
constructor() {
|
|
this.state = {
|
|
// User & Auth
|
|
user: null,
|
|
team: null,
|
|
role: null,
|
|
|
|
// Navigation
|
|
currentPage: 'dashboard',
|
|
currentProject: null, // New: Selected Project Context
|
|
sidebarOpen: true,
|
|
|
|
// Data
|
|
projects: [], // New: List of available projects
|
|
tokens: [],
|
|
components: [],
|
|
styles: [],
|
|
|
|
// Discovery
|
|
discovery: null,
|
|
health: null,
|
|
activity: [],
|
|
stats: null,
|
|
|
|
// Figma
|
|
figmaConnected: false,
|
|
figmaFileKey: null,
|
|
lastSync: null,
|
|
|
|
// Configuration (loaded from server /api/config)
|
|
serverConfig: null,
|
|
isConfigLoading: true,
|
|
configError: null,
|
|
|
|
// UI State
|
|
loading: {},
|
|
errors: {},
|
|
notifications: []
|
|
};
|
|
|
|
this.listeners = new Map();
|
|
this.middleware = [];
|
|
}
|
|
|
|
// === State Access ===
|
|
|
|
get(key) {
|
|
return key ? this.state[key] : this.state;
|
|
}
|
|
|
|
// === State Updates ===
|
|
|
|
set(updates, silent = false) {
|
|
const prevState = { ...this.state };
|
|
|
|
// Apply middleware
|
|
for (const mw of this.middleware) {
|
|
updates = mw(updates, prevState);
|
|
}
|
|
|
|
this.state = { ...this.state, ...updates };
|
|
|
|
if (!silent) {
|
|
this._notify(updates, prevState);
|
|
}
|
|
}
|
|
|
|
// === Subscriptions ===
|
|
|
|
subscribe(key, callback) {
|
|
if (!this.listeners.has(key)) {
|
|
this.listeners.set(key, new Set());
|
|
}
|
|
this.listeners.get(key).add(callback);
|
|
|
|
// Return unsubscribe function
|
|
return () => {
|
|
this.listeners.get(key)?.delete(callback);
|
|
};
|
|
}
|
|
|
|
subscribeAll(callback) {
|
|
return this.subscribe('*', callback);
|
|
}
|
|
|
|
_notify(updates, prevState) {
|
|
// Notify specific key listeners
|
|
for (const key of Object.keys(updates)) {
|
|
const listeners = this.listeners.get(key);
|
|
if (listeners) {
|
|
listeners.forEach(cb => cb(updates[key], prevState[key], key));
|
|
}
|
|
}
|
|
|
|
// Notify global listeners
|
|
const globalListeners = this.listeners.get('*');
|
|
if (globalListeners) {
|
|
globalListeners.forEach(cb => cb(this.state, prevState));
|
|
}
|
|
}
|
|
|
|
// === Middleware ===
|
|
|
|
use(middleware) {
|
|
this.middleware.push(middleware);
|
|
}
|
|
|
|
// === Loading State ===
|
|
|
|
setLoading(key, loading = true) {
|
|
this.set({
|
|
loading: { ...this.state.loading, [key]: loading }
|
|
});
|
|
}
|
|
|
|
isLoading(key) {
|
|
return this.state.loading[key] || false;
|
|
}
|
|
|
|
// === Error State ===
|
|
|
|
setError(key, error) {
|
|
this.set({
|
|
errors: { ...this.state.errors, [key]: error }
|
|
});
|
|
}
|
|
|
|
clearError(key) {
|
|
const errors = { ...this.state.errors };
|
|
delete errors[key];
|
|
this.set({ errors });
|
|
}
|
|
|
|
// === Notifications ===
|
|
|
|
notify(message, type = 'info', duration = 5000) {
|
|
const notification = {
|
|
id: Date.now(),
|
|
message,
|
|
type,
|
|
timestamp: new Date()
|
|
};
|
|
|
|
this.set({
|
|
notifications: [...this.state.notifications, notification]
|
|
});
|
|
|
|
if (duration > 0) {
|
|
setTimeout(() => this.dismissNotification(notification.id), duration);
|
|
}
|
|
|
|
return notification.id;
|
|
}
|
|
|
|
dismissNotification(id) {
|
|
this.set({
|
|
notifications: this.state.notifications.filter(n => n.id !== id)
|
|
});
|
|
}
|
|
|
|
// === User & Auth ===
|
|
|
|
setUser(user, team = null, role = null) {
|
|
this.set({ user, team, role });
|
|
}
|
|
|
|
logout() {
|
|
this.set({
|
|
user: null,
|
|
team: null,
|
|
role: null,
|
|
projects: [],
|
|
tokens: [],
|
|
components: [],
|
|
currentProject: null
|
|
});
|
|
localStorage.removeItem('currentProject');
|
|
}
|
|
|
|
hasPermission(permission) {
|
|
const rolePermissions = {
|
|
SUPER_ADMIN: ['*'],
|
|
TEAM_LEAD: ['read', 'write', 'sync', 'manage_team'],
|
|
DEVELOPER: ['read', 'write', 'sync'],
|
|
VIEWER: ['read']
|
|
};
|
|
|
|
const perms = rolePermissions[this.state.role] || [];
|
|
return perms.includes('*') || perms.includes(permission);
|
|
}
|
|
|
|
// === Projects (Enhanced for Phase 1) ===
|
|
|
|
/**
|
|
* Fetch all projects from API
|
|
*/
|
|
async fetchProjects() {
|
|
this.setLoading('projects', true);
|
|
try {
|
|
// Check for auth token (assuming basic auth structure or cookie based for now)
|
|
// If a specific token property exists on the store, use it.
|
|
// For now, we assume credentials are handled via cookies/standard fetch
|
|
const headers = { 'Content-Type': 'application/json' };
|
|
|
|
const response = await fetch('/api/projects', { headers });
|
|
|
|
if (!response.ok) throw new Error(`API Error: ${response.status}`);
|
|
|
|
const json = await response.json();
|
|
|
|
if (json.status === 'success') {
|
|
this.setProjects(json.data.projects || []);
|
|
|
|
// Auto-select first project if none selected and projects exist
|
|
if (!this.state.currentProject && json.data.projects.length > 0) {
|
|
// Check localStorage first
|
|
const stored = localStorage.getItem('currentProject');
|
|
if (stored) {
|
|
try {
|
|
const parsed = JSON.parse(stored);
|
|
const exists = json.data.projects.find(p => p.id === parsed.id);
|
|
if (exists) this.setProject(exists);
|
|
} catch (e) {
|
|
console.warn('Invalid stored project', e);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
throw new Error(json.message || 'Failed to fetch projects');
|
|
}
|
|
} catch (error) {
|
|
console.error('Project fetch error:', error);
|
|
this.setError('projects', error.message);
|
|
// Fallback for dev/demo mode if API fails
|
|
if (window.location.hostname === 'localhost' && this.state.projects.length === 0) {
|
|
console.warn('Using mock projects for development');
|
|
this.setProjects([
|
|
{ id: 'proj_default', name: 'Default Design System' },
|
|
{ id: 'proj_mobile', name: 'Mobile App DS' }
|
|
]);
|
|
}
|
|
} finally {
|
|
this.setLoading('projects', false);
|
|
}
|
|
}
|
|
|
|
setProjects(projects) {
|
|
this.set({ projects });
|
|
}
|
|
|
|
setProject(project) {
|
|
this.set({ currentProject: project });
|
|
if (project) {
|
|
localStorage.setItem('currentProject', JSON.stringify(project));
|
|
} else {
|
|
localStorage.removeItem('currentProject');
|
|
}
|
|
}
|
|
|
|
async getProjectConfig() {
|
|
if (!this.state.currentProject) return null;
|
|
|
|
this.setLoading('config', true);
|
|
try {
|
|
const response = await fetch(`/api/config/${this.state.currentProject.id}/resolved`);
|
|
const json = await response.json();
|
|
return json.status === 'success' ? json.data.config : null;
|
|
} catch (error) {
|
|
console.error('Config fetch error:', error);
|
|
return null;
|
|
} finally {
|
|
this.setLoading('config', false);
|
|
}
|
|
}
|
|
|
|
addProject(project) {
|
|
this.set({
|
|
projects: [...this.state.projects, project]
|
|
});
|
|
}
|
|
|
|
updateProject(id, updates) {
|
|
this.set({
|
|
projects: this.state.projects.map(p =>
|
|
p.id === id ? { ...p, ...updates } : p
|
|
)
|
|
});
|
|
}
|
|
|
|
// === Figma ===
|
|
|
|
setFigmaConnected(connected, fileKey = null) {
|
|
this.set({
|
|
figmaConnected: connected,
|
|
figmaFileKey: fileKey
|
|
});
|
|
}
|
|
|
|
setLastSync(timestamp) {
|
|
this.set({ lastSync: timestamp });
|
|
}
|
|
|
|
// === Tokens ===
|
|
|
|
setTokens(tokens) {
|
|
this.set({ tokens });
|
|
}
|
|
|
|
getTokensByCategory(category) {
|
|
return this.state.tokens.filter(t => t.category === category);
|
|
}
|
|
|
|
// === Components ===
|
|
|
|
setComponents(components) {
|
|
this.set({ components });
|
|
}
|
|
|
|
// === Discovery ===
|
|
|
|
setDiscovery(discovery) {
|
|
this.set({ discovery });
|
|
}
|
|
|
|
setHealth(health) {
|
|
this.set({ health });
|
|
}
|
|
|
|
setActivity(activity) {
|
|
this.set({ activity });
|
|
}
|
|
|
|
setStats(stats) {
|
|
this.set({ stats });
|
|
}
|
|
|
|
// === Persistence ===
|
|
|
|
persist() {
|
|
const toPersist = {
|
|
user: this.state.user,
|
|
team: this.state.team,
|
|
role: this.state.role,
|
|
figmaFileKey: this.state.figmaFileKey,
|
|
sidebarOpen: this.state.sidebarOpen
|
|
// Note: currentProject is persisted separately in setProject
|
|
};
|
|
localStorage.setItem('dss-store', JSON.stringify(toPersist));
|
|
}
|
|
|
|
hydrate() {
|
|
try {
|
|
// Hydrate general store data
|
|
const stored = localStorage.getItem('dss-store');
|
|
if (stored) {
|
|
const data = JSON.parse(stored);
|
|
this.set(data, true);
|
|
}
|
|
|
|
// Hydrate Project Context specifically
|
|
const storedProject = localStorage.getItem('currentProject');
|
|
if (storedProject) {
|
|
const project = JSON.parse(storedProject);
|
|
this.set({ currentProject: project }, true);
|
|
}
|
|
} catch (e) {
|
|
console.warn('Failed to hydrate store:', e);
|
|
}
|
|
}
|
|
|
|
// === Debug ===
|
|
|
|
debug() {
|
|
console.group('App Store State');
|
|
console.log('State:', this.state);
|
|
console.log('Listeners:', Array.from(this.listeners.keys()));
|
|
console.groupEnd();
|
|
}
|
|
}
|
|
|
|
// Logging middleware
|
|
const loggingMiddleware = (updates, prevState) => {
|
|
if (window.DEBUG) {
|
|
console.log('[Store Update]', updates);
|
|
}
|
|
return updates;
|
|
};
|
|
|
|
// Create and export singleton
|
|
const store = new AppStore();
|
|
store.use(loggingMiddleware);
|
|
store.hydrate();
|
|
|
|
// Auto-persist on important changes
|
|
store.subscribe('user', () => store.persist());
|
|
store.subscribe('team', () => store.persist());
|
|
store.subscribe('figmaFileKey', () => store.persist());
|
|
|
|
export { AppStore };
|
|
export default store;
|
|
</UPDATED_EXISTING_FILE>
|
|
|
|
<NEWFILE: admin-ui/js/components/layout/ds-project-selector.js>
|
|
/**
|
|
* ds-project-selector.js
|
|
*
|
|
* Dropdown component for switching between Design System projects.
|
|
* Persists selection to AppStore and LocalStorage.
|
|
*/
|
|
|
|
import store from '../../stores/app-store.js';
|
|
|
|
class DSProjectSelector extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.unsubscribe = null;
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
this.setupEventListeners();
|
|
|
|
// Subscribe to store changes
|
|
this.unsubscribe = store.subscribe('projects', () => this.updateOptions());
|
|
store.subscribe('currentProject', () => this.updateSelection());
|
|
store.subscribe('loading', () => this.updateLoadingState());
|
|
|
|
// Initial fetch
|
|
this.fetchProjects();
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.unsubscribe) this.unsubscribe();
|
|
}
|
|
|
|
async fetchProjects() {
|
|
await store.fetchProjects();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.project-selector-container {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
select.project-select {
|
|
appearance: none;
|
|
background-color: var(--vscode-dropdown-background, #252526);
|
|
color: var(--vscode-dropdown-foreground, #cccccc);
|
|
border: 1px solid var(--vscode-dropdown-border, #3c3c3c);
|
|
padding: 4px 24px 4px 8px;
|
|
border-radius: 3px;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
cursor: pointer;
|
|
min-width: 150px;
|
|
outline: none;
|
|
}
|
|
|
|
select.project-select:focus {
|
|
border-color: var(--vscode-focusBorder, #007fd4);
|
|
}
|
|
|
|
/* Custom arrow implementation since we removed default appearance */
|
|
.select-arrow {
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
pointer-events: none;
|
|
font-size: 10px;
|
|
color: var(--vscode-foreground, #cccccc);
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 12px;
|
|
height: 12px;
|
|
border: 2px solid var(--vscode-text-dim, #666);
|
|
border-top-color: var(--vscode-accent, #007fd4);
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
display: none;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
</style>
|
|
|
|
<div class="project-selector-container">
|
|
<select class="project-select" id="project-dropdown" aria-label="Select Project">
|
|
<option value="" disabled selected>Select Project...</option>
|
|
</select>
|
|
<div class="select-arrow">▼</div>
|
|
<div class="loading-spinner" id="project-loader"></div>
|
|
</div>
|
|
`;
|
|
|
|
// Initial population if data exists
|
|
this.updateOptions();
|
|
this.updateSelection();
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const select = this.querySelector('#project-dropdown');
|
|
if (select) {
|
|
select.addEventListener('change', (e) => {
|
|
const projectId = e.target.value;
|
|
const projects = store.get('projects');
|
|
const selectedProject = projects.find(p => p.id === projectId);
|
|
|
|
if (selectedProject) {
|
|
store.setProject(selectedProject);
|
|
// Emit event for immediate listeners
|
|
this.dispatchEvent(new CustomEvent('project-changed', {
|
|
detail: { project: selectedProject },
|
|
bubbles: true,
|
|
composed: true
|
|
}));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
updateOptions() {
|
|
const select = this.querySelector('#project-dropdown');
|
|
if (!select) return;
|
|
|
|
const projects = store.get('projects');
|
|
const currentProject = store.get('currentProject');
|
|
|
|
// Keep the placeholder as first option
|
|
select.innerHTML = '<option value="" disabled>Select Project...</option>';
|
|
|
|
if (projects.length === 0) {
|
|
const emptyOpt = document.createElement('option');
|
|
emptyOpt.text = "No projects found";
|
|
emptyOpt.disabled = true;
|
|
select.appendChild(emptyOpt);
|
|
} else {
|
|
projects.forEach(proj => {
|
|
const option = document.createElement('option');
|
|
option.value = proj.id;
|
|
option.textContent = proj.name;
|
|
select.appendChild(option);
|
|
});
|
|
}
|
|
|
|
// Re-apply selection if exists
|
|
if (currentProject) {
|
|
select.value = currentProject.id;
|
|
} else {
|
|
select.value = "";
|
|
}
|
|
}
|
|
|
|
updateSelection() {
|
|
const select = this.querySelector('#project-dropdown');
|
|
const currentProject = store.get('currentProject');
|
|
|
|
if (select && currentProject) {
|
|
select.value = currentProject.id;
|
|
}
|
|
}
|
|
|
|
updateLoadingState() {
|
|
const isLoading = store.isLoading('projects');
|
|
const loader = this.querySelector('#project-loader');
|
|
const arrow = this.querySelector('.select-arrow');
|
|
|
|
if (loader && arrow) {
|
|
loader.style.display = isLoading ? 'block' : 'none';
|
|
arrow.style.display = isLoading ? 'none' : 'block';
|
|
}
|
|
}
|
|
}
|
|
|
|
customElements.define('ds-project-selector', DSProjectSelector);
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/projects/ProjectsModule.js>
|
|
/**
|
|
* ProjectsModule.js
|
|
* Feature module for managing DSS projects, metadata, and selection.
|
|
*/
|
|
|
|
class ProjectsModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container {
|
|
padding: 24px;
|
|
color: var(--vscode-foreground);
|
|
}
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border, #3c3c3c);
|
|
border-radius: 6px;
|
|
background-color: var(--vscode-editor-background);
|
|
margin-top: 24px;
|
|
}
|
|
h1 {
|
|
font-size: 24px;
|
|
font-weight: 500;
|
|
margin-bottom: 8px;
|
|
}
|
|
.icon-large {
|
|
font-size: 48px;
|
|
margin-bottom: 16px;
|
|
opacity: 0.8;
|
|
}
|
|
p {
|
|
color: var(--vscode-descriptionForeground);
|
|
max-width: 500px;
|
|
text-align: center;
|
|
line-height: 1.5;
|
|
}
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Projects</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">📁</div>
|
|
<h3>Projects Module Under Construction</h3>
|
|
<p>
|
|
This module will allow you to create new projects, edit project metadata,
|
|
and manage access controls. Currently, use the selector in the top bar to switch contexts.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-projects-module', ProjectsModule);
|
|
export default ProjectsModule;
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/config/ConfigModule.js>
|
|
/**
|
|
* ConfigModule.js
|
|
* Feature module for managing the 3-tier cascade configuration.
|
|
*/
|
|
|
|
class ConfigModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container { padding: 24px; color: var(--vscode-foreground); }
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border);
|
|
border-radius: 6px;
|
|
margin-top: 24px;
|
|
}
|
|
.icon-large { font-size: 48px; margin-bottom: 16px; }
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Configuration</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">⚙️</div>
|
|
<h3>Configuration Module Under Construction</h3>
|
|
<p>
|
|
Manage your 3-tier configuration cascade here (Base → Brand → Project).
|
|
Future updates will include visual editors for JSON/YAML config files.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-config-module', ConfigModule);
|
|
export default ConfigModule;
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/components/ComponentsModule.js>
|
|
/**
|
|
* ComponentsModule.js
|
|
* Feature module for the Component Registry, search, and status tracking.
|
|
*/
|
|
|
|
class ComponentsModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container { padding: 24px; color: var(--vscode-foreground); }
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border);
|
|
border-radius: 6px;
|
|
margin-top: 24px;
|
|
}
|
|
.icon-large { font-size: 48px; margin-bottom: 16px; }
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Components</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">🧩</div>
|
|
<h3>Components Registry Under Construction</h3>
|
|
<p>
|
|
View component status, health, and documentation links.
|
|
This module will interface with the registry to show what is available in your design system.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-components-module', ComponentsModule);
|
|
export default ComponentsModule;
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/translations/TranslationsModule.js>
|
|
/**
|
|
* TranslationsModule.js
|
|
* Feature module for managing Legacy → DSS token mappings (Principle #2).
|
|
*/
|
|
|
|
class TranslationsModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container { padding: 24px; color: var(--vscode-foreground); }
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border);
|
|
border-radius: 6px;
|
|
margin-top: 24px;
|
|
}
|
|
.icon-large { font-size: 48px; margin-bottom: 16px; }
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Translation Dictionaries</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">🔄</div>
|
|
<h3>Translations Module Under Construction</h3>
|
|
<p>
|
|
<b>Core DSS Principle #2:</b> Map legacy design tokens to modern DSS tokens.
|
|
This interface will allow creation and validation of translation dictionaries.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-translations-module', TranslationsModule);
|
|
export default TranslationsModule;
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/discovery/DiscoveryModule.js>
|
|
/**
|
|
* DiscoveryModule.js
|
|
* Feature module for scanning projects, activity logs, and analysis.
|
|
*/
|
|
|
|
class DiscoveryModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container { padding: 24px; color: var(--vscode-foreground); }
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border);
|
|
border-radius: 6px;
|
|
margin-top: 24px;
|
|
}
|
|
.icon-large { font-size: 48px; margin-bottom: 16px; }
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Discovery</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">🔍</div>
|
|
<h3>Discovery Module Under Construction</h3>
|
|
<p>
|
|
Run scanners, view codebase analytics, and monitor system activity.
|
|
Integration with the scanner backend is coming in the next phase.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-discovery-module', DiscoveryModule);
|
|
export default DiscoveryModule;
|
|
</NEWFILE>
|
|
|
|
<NEWFILE: admin-ui/js/modules/admin/AdminModule.js>
|
|
/**
|
|
* AdminModule.js
|
|
* Feature module for RBAC, user management, and system audit logs.
|
|
*/
|
|
|
|
class AdminModule extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this.render();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = `
|
|
<style>
|
|
.module-container { padding: 24px; color: var(--vscode-foreground); }
|
|
.empty-state {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 48px;
|
|
border: 1px dashed var(--vscode-input-border);
|
|
border-radius: 6px;
|
|
margin-top: 24px;
|
|
}
|
|
.icon-large { font-size: 48px; margin-bottom: 16px; }
|
|
</style>
|
|
|
|
<div class="module-container">
|
|
<h1>Administration</h1>
|
|
<div class="empty-state">
|
|
<div class="icon-large">👤</div>
|
|
<h3>Admin Module Under Construction</h3>
|
|
<p>
|
|
Manage users, permissions (RBAC), and view audit logs.
|
|
Only available to users with administrative privileges.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define('dss-admin-module', AdminModule);
|
|
export default AdminModule;
|
|
</NEWFILE>
|
|
|
|
<UPDATED_EXISTING_FILE: admin-ui/js/core/router.js>
|
|
/**
|
|
* DSS Router
|
|
*
|
|
* Centralized hash-based routing with guards, lifecycle hooks, and
|
|
* declarative route definitions for enterprise-grade navigation management.
|
|
*
|
|
* @module router
|
|
*/
|
|
|
|
import { notifyError, ErrorCode } from './messaging.js';
|
|
|
|
/**
|
|
* Route configuration object
|
|
* @typedef {Object} RouteConfig
|
|
* @property {string} path - Route path (e.g., '/dashboard', '/projects')
|
|
* @property {string} name - Route name for programmatic navigation
|
|
* @property {Function} handler - Route handler function
|
|
* @property {Function} [beforeEnter] - Guard called before entering route
|
|
* @property {Function} [afterEnter] - Hook called after entering route
|
|
* @property {Function} [onLeave] - Hook called when leaving route
|
|
* @property {Object} [meta] - Route metadata
|
|
*/
|
|
|
|
/**
|
|
* Router class for centralized route management
|
|
*/
|
|
class Router {
|
|
constructor() {
|
|
this.routes = new Map();
|
|
this.currentRoute = null;
|
|
this.previousRoute = null;
|
|
this.defaultRoute = 'projects'; // Updated default route for new architecture
|
|
this.isNavigating = false;
|
|
|
|
// Bind handlers
|
|
this.handleHashChange = this.handleHashChange.bind(this);
|
|
this.handlePopState = this.handlePopState.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Initialize the router
|
|
*/
|
|
init() {
|
|
// Define Core Routes for New Architecture
|
|
this.registerAll([
|
|
{
|
|
path: '/projects',
|
|
name: 'Projects',
|
|
handler: () => this.loadModule('dss-projects-module', () => import('../modules/projects/ProjectsModule.js'))
|
|
},
|
|
{
|
|
path: '/config',
|
|
name: 'Configuration',
|
|
handler: () => this.loadModule('dss-config-module', () => import('../modules/config/ConfigModule.js'))
|
|
},
|
|
{
|
|
path: '/components',
|
|
name: 'Components',
|
|
handler: () => this.loadModule('dss-components-module', () => import('../modules/components/ComponentsModule.js'))
|
|
},
|
|
{
|
|
path: '/translations',
|
|
name: 'Translations',
|
|
handler: () => this.loadModule('dss-translations-module', () => import('../modules/translations/TranslationsModule.js'))
|
|
},
|
|
{
|
|
path: '/discovery',
|
|
name: 'Discovery',
|
|
handler: () => this.loadModule('dss-discovery-module', () => import('../modules/discovery/DiscoveryModule.js'))
|
|
},
|
|
{
|
|
path: '/admin',
|
|
name: 'Admin',
|
|
handler: () => this.loadModule('dss-admin-module', () => import('../modules/admin/AdminModule.js'))
|
|
}
|
|
]);
|
|
|
|
// Listen for hash changes
|
|
window.addEventListener('hashchange', this.handleHashChange);
|
|
window.addEventListener('popstate', this.handlePopState);
|
|
|
|
// Handle initial route
|
|
this.handleHashChange();
|
|
}
|
|
|
|
/**
|
|
* Helper to load dynamic modules into the stage
|
|
* @param {string} tagName - Custom element tag name
|
|
* @param {Function} importFn - Dynamic import function
|
|
*/
|
|
async loadModule(tagName, importFn) {
|
|
try {
|
|
// 1. Load the module file
|
|
await importFn();
|
|
|
|
// 2. Update the stage content
|
|
const stageContent = document.querySelector('#stage-workdesk-content');
|
|
if (stageContent) {
|
|
stageContent.innerHTML = '';
|
|
const element = document.createElement(tagName);
|
|
stageContent.appendChild(element);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Failed to load module ${tagName}:`, error);
|
|
notifyError(`Failed to load module`, ErrorCode.SYSTEM_UNEXPECTED);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a route
|
|
* @param {RouteConfig} config - Route configuration
|
|
*/
|
|
register(config) {
|
|
if (!config.path) {
|
|
throw new Error('Route path is required');
|
|
}
|
|
|
|
if (!config.handler) {
|
|
throw new Error('Route handler is required');
|
|
}
|
|
|
|
// Normalize path (remove leading slash for hash routing)
|
|
const path = config.path.replace(/^\//, '');
|
|
|
|
this.routes.set(path, {
|
|
path,
|
|
name: config.name || path,
|
|
handler: config.handler,
|
|
beforeEnter: config.beforeEnter || null,
|
|
afterEnter: config.afterEnter || null,
|
|
onLeave: config.onLeave || null,
|
|
meta: config.meta || {},
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Register multiple routes
|
|
* @param {RouteConfig[]} routes - Array of route configurations
|
|
*/
|
|
registerAll(routes) {
|
|
routes.forEach(route => this.register(route));
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set default route
|
|
* @param {string} path - Default route path
|
|
*/
|
|
setDefaultRoute(path) {
|
|
this.defaultRoute = path.replace(/^\//, '');
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Navigate to a route
|
|
* @param {string} path - Route path or name
|
|
* @param {Object} [options] - Navigation options
|
|
* @param {boolean} [options.replace] - Replace history instead of push
|
|
* @param {Object} [options.state] - State to pass to route
|
|
*/
|
|
navigate(path, options = {}) {
|
|
const normalizedPath = path.replace(/^\//, '');
|
|
|
|
// Update hash
|
|
if (options.replace) {
|
|
window.location.replace(`#${normalizedPath}`);
|
|
} else {
|
|
window.location.hash = normalizedPath;
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Navigate to a route by name
|
|
* @param {string} name - Route name
|
|
* @param {Object} [options] - Navigation options
|
|
*/
|
|
navigateByName(name, options = {}) {
|
|
const route = Array.from(this.routes.values()).find(r => r.name === name);
|
|
|
|
if (!route) {
|
|
notifyError(`Route not found: ${name}`, ErrorCode.SYSTEM_UNEXPECTED);
|
|
return this;
|
|
}
|
|
|
|
return this.navigate(route.path, options);
|
|
}
|
|
|
|
/**
|
|
* Handle hash change event
|
|
*/
|
|
async handleHashChange() {
|
|
if (this.isNavigating) return;
|
|
|
|
this.isNavigating = true;
|
|
|
|
try {
|
|
// Get path from hash
|
|
let path = window.location.hash.replace(/^#/, '') || this.defaultRoute;
|
|
|
|
// Extract route and params
|
|
const { routePath, params } = this.parseRoute(path);
|
|
|
|
// Find route
|
|
const route = this.routes.get(routePath);
|
|
|
|
if (!route) {
|
|
console.warn(`Route not registered: ${routePath}, falling back to default`);
|
|
this.navigate(this.defaultRoute, { replace: true });
|
|
return;
|
|
}
|
|
|
|
// Call onLeave hook for previous route
|
|
if (this.currentRoute && this.currentRoute.onLeave) {
|
|
await this.callHook(this.currentRoute.onLeave, { from: this.currentRoute, to: route });
|
|
}
|
|
|
|
// Call beforeEnter guard
|
|
if (route.beforeEnter) {
|
|
const canEnter = await this.callGuard(route.beforeEnter, { route, params });
|
|
|
|
if (!canEnter) {
|
|
// Guard rejected, stay on current route or go to default
|
|
if (this.currentRoute) {
|
|
this.navigate(this.currentRoute.path, { replace: true });
|
|
} else {
|
|
this.navigate(this.defaultRoute, { replace: true });
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Update route tracking
|
|
this.previousRoute = this.currentRoute;
|
|
this.currentRoute = route;
|
|
|
|
// Call route handler
|
|
await route.handler({ route, params, router: this });
|
|
|
|
// Call afterEnter hook
|
|
if (route.afterEnter) {
|
|
await this.callHook(route.afterEnter, { route, params, from: this.previousRoute });
|
|
}
|
|
|
|
// Emit route change event
|
|
this.emitRouteChange(route, params);
|
|
|
|
} catch (error) {
|
|
console.error('Router navigation error:', error);
|
|
notifyError('Navigation failed', ErrorCode.SYSTEM_UNEXPECTED, {
|
|
path: window.location.hash,
|
|
error: error.message,
|
|
});
|
|
} finally {
|
|
this.isNavigating = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle popstate event (browser back/forward)
|
|
*/
|
|
handlePopState(event) {
|
|
this.handleHashChange();
|
|
}
|
|
|
|
/**
|
|
* Parse route path and extract params
|
|
* @param {string} path - Route path
|
|
* @returns {Object} Route path and params
|
|
*/
|
|
parseRoute(path) {
|
|
// For now, simple implementation - just return the path
|
|
// Can be extended to support params like '/projects/:id'
|
|
const [routePath, queryString] = path.split('?');
|
|
|
|
const params = {};
|
|
if (queryString) {
|
|
new URLSearchParams(queryString).forEach((value, key) => {
|
|
params[key] = value;
|
|
});
|
|
}
|
|
|
|
return { routePath, params };
|
|
}
|
|
|
|
/**
|
|
* Call a route guard
|
|
* @param {Function} guard - Guard function
|
|
* @param {Object} context - Guard context
|
|
* @returns {Promise<boolean>} Whether navigation should proceed
|
|
*/
|
|
async callGuard(guard, context) {
|
|
try {
|
|
const result = await guard(context);
|
|
return result !== false; // Undefined or true = proceed
|
|
} catch (error) {
|
|
console.error('Route guard error:', error);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call a lifecycle hook
|
|
* @param {Function} hook - Hook function
|
|
* @param {Object} context - Hook context
|
|
*/
|
|
async callHook(hook, context) {
|
|
try {
|
|
await hook(context);
|
|
} catch (error) {
|
|
console.error('Route hook error:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Emit route change event
|
|
* @param {Object} route - Current route
|
|
* @param {Object} params - Route params
|
|
*/
|
|
emitRouteChange(route, params) {
|
|
window.dispatchEvent(new CustomEvent('route-changed', {
|
|
detail: {
|
|
route,
|
|
params,
|
|
previous: this.previousRoute,
|
|
}
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Get current route
|
|
* @returns {Object|null} Current route config
|
|
*/
|
|
getCurrentRoute() {
|
|
return this.currentRoute;
|
|
}
|
|
|
|
/**
|
|
* Get previous route
|
|
* @returns {Object|null} Previous route config
|
|
*/
|
|
getPreviousRoute() {
|
|
return this.previousRoute;
|
|
}
|
|
|
|
/**
|
|
* Check if a route exists
|
|
* @param {string} path - Route path
|
|
* @returns {boolean} Whether route exists
|
|
*/
|
|
hasRoute(path) {
|
|
const normalizedPath = path.replace(/^\//, '');
|
|
return this.routes.has(normalizedPath);
|
|
}
|
|
|
|
/**
|
|
* Get route by path
|
|
* @param {string} path - Route path
|
|
* @returns {Object|null} Route config
|
|
*/
|
|
getRoute(path) {
|
|
const normalizedPath = path.replace(/^\//, '');
|
|
return this.routes.get(normalizedPath) || null;
|
|
}
|
|
|
|
/**
|
|
* Get all routes
|
|
* @returns {Array} All registered routes
|
|
*/
|
|
getAllRoutes() {
|
|
return Array.from(this.routes.values());
|
|
}
|
|
|
|
/**
|
|
* Go back in history
|
|
*/
|
|
back() {
|
|
window.history.back();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Go forward in history
|
|
*/
|
|
forward() {
|
|
window.history.forward();
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Destroy the router (cleanup)
|
|
*/
|
|
destroy() {
|
|
window.removeEventListener('hashchange', this.handleHashChange);
|
|
window.removeEventListener('popstate', this.handlePopState);
|
|
this.routes.clear();
|
|
this.currentRoute = null;
|
|
this.previousRoute = null;
|
|
}
|
|
}
|
|
|
|
// Singleton instance
|
|
const router = new Router();
|
|
|
|
// Export both the instance and the class
|
|
export { Router };
|
|
export default router;
|
|
|
|
/**
|
|
* Common route guards
|
|
*/
|
|
export const guards = {
|
|
/**
|
|
* Require authentication
|
|
*/
|
|
requireAuth({ route }) {
|
|
// Check if user is authenticated
|
|
// For now, always allow (implement auth later)
|
|
return true;
|
|
},
|
|
|
|
/**
|
|
* Require specific permission
|
|
*/
|
|
requirePermission(permission) {
|
|
return ({ route }) => {
|
|
// Check if user has permission
|
|
// For now, always allow (implement RBAC later)
|
|
return true;
|
|
};
|
|
},
|
|
|
|
/**
|
|
* Require project selected
|
|
*/
|
|
requireProject({ route, params }) {
|
|
const projectId = localStorage.getItem('currentProject');
|
|
|
|
if (!projectId) {
|
|
notifyError('Please select a project first', ErrorCode.USER_ACTION_FORBIDDEN);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
};
|
|
</UPDATED_EXISTING_FILE>
|
|
|
|
<UPDATED_EXISTING_FILE: admin-ui/js/components/layout/ds-shell.js>
|
|
/**
|
|
* ds-shell.js
|
|
* Main shell component - provides IDE-style grid layout
|
|
* Refactored for Phase 1 Architecture (Feature-based Modules)
|
|
*/
|
|
|
|
import './ds-activity-bar.js';
|
|
import './ds-panel.js';
|
|
import './ds-project-selector.js'; // New Project Selector
|
|
import './ds-ai-chat-sidebar.js';
|
|
import '../admin/ds-user-settings.js';
|
|
import '../ds-notification-center.js';
|
|
import router from '../../core/router.js'; // Import Router
|
|
import layoutManager from '../../core/layout-manager.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';
|
|
import { authReady } from '../../utils/demo-auth-init.js';
|
|
|
|
class DSShell extends HTMLElement {
|
|
constructor() {
|
|
super();
|
|
this.browserInitialized = false;
|
|
this.currentView = 'module';
|
|
|
|
// MVP2: Initialize stores
|
|
this.adminStore = useAdminStore();
|
|
this.projectStore = useProjectStore();
|
|
this.userStore = useUserStore();
|
|
}
|
|
|
|
async connectedCallback() {
|
|
// Render UI immediately (non-blocking)
|
|
this.render();
|
|
this.setupEventListeners();
|
|
|
|
// Initialize layout manager
|
|
// Keep this for now to maintain layout structure, though "workdesks" are being phased out
|
|
if (layoutManager && typeof layoutManager.init === 'function') {
|
|
layoutManager.init(this);
|
|
}
|
|
|
|
// Initialize Router
|
|
router.init();
|
|
|
|
// Wait for authentication
|
|
console.log('[DSShell] Waiting for authentication...');
|
|
const authResult = await authReady;
|
|
console.log('[DSShell] Authentication complete:', authResult);
|
|
|
|
// Initialize services
|
|
notificationService.init();
|
|
|
|
// Set initial active link
|
|
this.updateActiveLink();
|
|
}
|
|
|
|
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() {
|
|
this.setupMobileMenu();
|
|
this.setupHeaderActions();
|
|
this.setupNavigationHighlight();
|
|
|
|
// Listen for route changes to update UI
|
|
window.addEventListener('route-changed', (e) => {
|
|
this.updateActiveLink(e.detail.route.path);
|
|
});
|
|
}
|
|
|
|
setupNavigationHighlight() {
|
|
const navItems = this.querySelectorAll('.nav-item');
|
|
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)';
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
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-selection)';
|
|
item.style.color = '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';
|
|
}
|
|
});
|
|
}
|
|
|
|
setupHeaderActions() {
|
|
const stageActions = this.querySelector('#stage-actions');
|
|
if (!stageActions) return;
|
|
|
|
stageActions.innerHTML = `
|
|
<button id="chat-toggle-btn" aria-label="Toggle AI Chat" title="Toggle Chat" style="background:transparent; border:none; color:var(--vscode-text-dim); cursor:pointer; padding:6px; font-size:16px;">💬</button>
|
|
<div style="position: relative;">
|
|
<button id="notification-toggle-btn" aria-label="Notifications" title="Notifications" style="background:transparent; border:none; color:var(--vscode-text-dim); cursor:pointer; padding:6px; font-size:16px;">🔔</button>
|
|
<ds-notification-center></ds-notification-center>
|
|
</div>
|
|
<button id="settings-btn" aria-label="Settings" title="Settings" style="background:transparent; border:none; color:var(--vscode-text-dim); cursor:pointer; padding:6px; font-size:16px;">⚙️</button>
|
|
`;
|
|
|
|
// Re-attach listeners for these buttons similar to original implementation
|
|
// Simplified for brevity, ensuring critical paths work
|
|
const chatBtn = this.querySelector('#chat-toggle-btn');
|
|
chatBtn?.addEventListener('click', () => {
|
|
const sidebar = this.querySelector('ds-ai-chat-sidebar');
|
|
if (sidebar && sidebar.toggleCollapse) sidebar.toggleCollapse();
|
|
});
|
|
|
|
const notifBtn = this.querySelector('#notification-toggle-btn');
|
|
const notifCenter = this.querySelector('ds-notification-center');
|
|
notifBtn?.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
if (notifCenter) {
|
|
if (notifCenter.hasAttribute('open')) notifCenter.removeAttribute('open');
|
|
else notifCenter.setAttribute('open', '');
|
|
}
|
|
});
|
|
|
|
// Close notifications on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (notifCenter && !notifCenter.contains(e.target) && !notifBtn.contains(e.target)) {
|
|
notifCenter.removeAttribute('open');
|
|
}
|
|
});
|
|
|
|
const settingsBtn = this.querySelector('#settings-btn');
|
|
settingsBtn?.addEventListener('click', () => {
|
|
// Simple navigation to admin/settings
|
|
router.navigate('admin');
|
|
});
|
|
}
|
|
|
|
setupMobileMenu() {
|
|
const hamburgerBtn = this.querySelector('#hamburger-menu');
|
|
const sidebar = this.querySelector('ds-sidebar');
|
|
|
|
if (hamburgerBtn && sidebar) {
|
|
// Update visibility based on screen size
|
|
const checkSize = () => {
|
|
if (window.innerWidth <= 768) {
|
|
hamburgerBtn.style.display = 'block';
|
|
} else {
|
|
hamburgerBtn.style.display = 'none';
|
|
sidebar.classList.remove('mobile-open');
|
|
}
|
|
};
|
|
|
|
window.addEventListener('resize', checkSize);
|
|
checkSize();
|
|
|
|
hamburgerBtn.addEventListener('click', () => {
|
|
sidebar.classList.toggle('mobile-open');
|
|
});
|
|
}
|
|
}
|
|
|
|
// Getters for compatibility with any legacy code looking for these elements
|
|
get sidebarContent() {
|
|
return this.querySelector('.sidebar-content');
|
|
}
|
|
|
|
get stageContent() {
|
|
return this.querySelector('#stage-workdesk-content');
|
|
}
|
|
}
|
|
|
|
customElements.define('ds-shell', DSShell);
|
|
</UPDATED_EXISTING_FILE>
|
|
</GENERATED-CODE>
|