Files
dss/zen_generated.code
Digital Production Factory 276ed71f31 Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm

Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)

Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability

Migration completed: $(date)
🤖 Clean migration with full functionality preserved
2025-12-09 18:45:48 -03:00

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>