Add comprehensive CSS styles for UI components and dark mode support

This commit is contained in:
rUv
2025-06-07 13:28:02 +00:00
parent 90f03bac7d
commit 6fe0d42f90
22 changed files with 5992 additions and 187 deletions

View File

@@ -0,0 +1,309 @@
// Dashboard Tab Component
import { healthService } from '../services/health.service.js';
import { poseService } from '../services/pose.service.js';
export class DashboardTab {
constructor(containerElement) {
this.container = containerElement;
this.statsElements = {};
this.healthSubscription = null;
this.statsInterval = null;
}
// Initialize component
async init() {
this.cacheElements();
await this.loadInitialData();
this.startMonitoring();
}
// Cache DOM elements
cacheElements() {
// System stats
const statsContainer = this.container.querySelector('.system-stats');
if (statsContainer) {
this.statsElements = {
bodyRegions: statsContainer.querySelector('[data-stat="body-regions"] .stat-value'),
samplingRate: statsContainer.querySelector('[data-stat="sampling-rate"] .stat-value'),
accuracy: statsContainer.querySelector('[data-stat="accuracy"] .stat-value'),
hardwareCost: statsContainer.querySelector('[data-stat="hardware-cost"] .stat-value')
};
}
// Status indicators
this.statusElements = {
apiStatus: this.container.querySelector('.api-status'),
streamStatus: this.container.querySelector('.stream-status'),
hardwareStatus: this.container.querySelector('.hardware-status')
};
}
// Load initial data
async loadInitialData() {
try {
// Get API info
const info = await healthService.getApiInfo();
this.updateApiInfo(info);
// Get current stats
const stats = await poseService.getStats(1);
this.updateStats(stats);
} catch (error) {
console.error('Failed to load dashboard data:', error);
this.showError('Failed to load dashboard data');
}
}
// Start monitoring
startMonitoring() {
// Subscribe to health updates
this.healthSubscription = healthService.subscribeToHealth(health => {
this.updateHealthStatus(health);
});
// Start periodic stats updates
this.statsInterval = setInterval(() => {
this.updateLiveStats();
}, 5000);
// Start health monitoring
healthService.startHealthMonitoring(30000);
}
// Update API info display
updateApiInfo(info) {
// Update version
const versionElement = this.container.querySelector('.api-version');
if (versionElement && info.version) {
versionElement.textContent = `v${info.version}`;
}
// Update environment
const envElement = this.container.querySelector('.api-environment');
if (envElement && info.environment) {
envElement.textContent = info.environment;
envElement.className = `api-environment env-${info.environment}`;
}
// Update features status
if (info.features) {
this.updateFeatures(info.features);
}
}
// Update features display
updateFeatures(features) {
const featuresContainer = this.container.querySelector('.features-status');
if (!featuresContainer) return;
featuresContainer.innerHTML = '';
Object.entries(features).forEach(([feature, enabled]) => {
const featureElement = document.createElement('div');
featureElement.className = `feature-item ${enabled ? 'enabled' : 'disabled'}`;
featureElement.innerHTML = `
<span class="feature-name">${this.formatFeatureName(feature)}</span>
<span class="feature-status">${enabled ? '✓' : '✗'}</span>
`;
featuresContainer.appendChild(featureElement);
});
}
// Update health status
updateHealthStatus(health) {
if (!health) return;
// Update overall status
const overallStatus = this.container.querySelector('.overall-health');
if (overallStatus) {
overallStatus.className = `overall-health status-${health.status}`;
overallStatus.textContent = health.status.toUpperCase();
}
// Update component statuses
if (health.components) {
Object.entries(health.components).forEach(([component, status]) => {
this.updateComponentStatus(component, status);
});
}
// Update metrics
if (health.metrics) {
this.updateSystemMetrics(health.metrics);
}
}
// Update component status
updateComponentStatus(component, status) {
const element = this.container.querySelector(`[data-component="${component}"]`);
if (element) {
element.className = `component-status status-${status.status}`;
element.querySelector('.status-text').textContent = status.status;
if (status.message) {
element.querySelector('.status-message').textContent = status.message;
}
}
}
// Update system metrics
updateSystemMetrics(metrics) {
// CPU usage
const cpuElement = this.container.querySelector('.cpu-usage');
if (cpuElement && metrics.cpu_percent !== undefined) {
cpuElement.textContent = `${metrics.cpu_percent.toFixed(1)}%`;
this.updateProgressBar('cpu', metrics.cpu_percent);
}
// Memory usage
const memoryElement = this.container.querySelector('.memory-usage');
if (memoryElement && metrics.memory_percent !== undefined) {
memoryElement.textContent = `${metrics.memory_percent.toFixed(1)}%`;
this.updateProgressBar('memory', metrics.memory_percent);
}
// Disk usage
const diskElement = this.container.querySelector('.disk-usage');
if (diskElement && metrics.disk_percent !== undefined) {
diskElement.textContent = `${metrics.disk_percent.toFixed(1)}%`;
this.updateProgressBar('disk', metrics.disk_percent);
}
}
// Update progress bar
updateProgressBar(type, percent) {
const progressBar = this.container.querySelector(`.progress-bar[data-type="${type}"]`);
if (progressBar) {
const fill = progressBar.querySelector('.progress-fill');
if (fill) {
fill.style.width = `${percent}%`;
fill.className = `progress-fill ${this.getProgressClass(percent)}`;
}
}
}
// Get progress class based on percentage
getProgressClass(percent) {
if (percent >= 90) return 'critical';
if (percent >= 75) return 'warning';
return 'normal';
}
// Update live statistics
async updateLiveStats() {
try {
// Get current pose data
const currentPose = await poseService.getCurrentPose();
this.updatePoseStats(currentPose);
// Get zones summary
const zonesSummary = await poseService.getZonesSummary();
this.updateZonesDisplay(zonesSummary);
} catch (error) {
console.error('Failed to update live stats:', error);
}
}
// Update pose statistics
updatePoseStats(poseData) {
if (!poseData) return;
// Update person count
const personCount = this.container.querySelector('.person-count');
if (personCount) {
personCount.textContent = poseData.total_persons || 0;
}
// Update average confidence
const avgConfidence = this.container.querySelector('.avg-confidence');
if (avgConfidence && poseData.persons) {
const confidences = poseData.persons.map(p => p.confidence);
const avg = confidences.length > 0
? (confidences.reduce((a, b) => a + b, 0) / confidences.length * 100).toFixed(1)
: 0;
avgConfidence.textContent = `${avg}%`;
}
}
// Update zones display
updateZonesDisplay(zonesSummary) {
const zonesContainer = this.container.querySelector('.zones-summary');
if (!zonesContainer || !zonesSummary) return;
zonesContainer.innerHTML = '';
Object.entries(zonesSummary.zones).forEach(([zoneId, data]) => {
const zoneElement = document.createElement('div');
zoneElement.className = 'zone-item';
zoneElement.innerHTML = `
<span class="zone-name">${data.name || zoneId}</span>
<span class="zone-count">${data.person_count}</span>
`;
zonesContainer.appendChild(zoneElement);
});
}
// Update statistics
updateStats(stats) {
if (!stats) return;
// Update detection count
const detectionCount = this.container.querySelector('.detection-count');
if (detectionCount && stats.total_detections !== undefined) {
detectionCount.textContent = this.formatNumber(stats.total_detections);
}
// Update accuracy if available
if (this.statsElements.accuracy && stats.average_confidence !== undefined) {
this.statsElements.accuracy.textContent = `${(stats.average_confidence * 100).toFixed(1)}%`;
}
}
// Format feature name
formatFeatureName(name) {
return name.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
// Format large numbers
formatNumber(num) {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
}
if (num >= 1000) {
return `${(num / 1000).toFixed(1)}K`;
}
return num.toString();
}
// Show error message
showError(message) {
const errorContainer = this.container.querySelector('.error-container');
if (errorContainer) {
errorContainer.textContent = message;
errorContainer.style.display = 'block';
setTimeout(() => {
errorContainer.style.display = 'none';
}, 5000);
}
}
// Clean up
dispose() {
if (this.healthSubscription) {
this.healthSubscription();
}
if (this.statsInterval) {
clearInterval(this.statsInterval);
}
healthService.stopHealthMonitoring();
}
}

View File

@@ -0,0 +1,165 @@
// Hardware Tab Component
export class HardwareTab {
constructor(containerElement) {
this.container = containerElement;
this.antennas = [];
this.csiUpdateInterval = null;
this.isActive = false;
}
// Initialize component
init() {
this.setupAntennas();
this.startCSISimulation();
}
// Set up antenna interactions
setupAntennas() {
this.antennas = Array.from(this.container.querySelectorAll('.antenna'));
this.antennas.forEach(antenna => {
antenna.addEventListener('click', () => {
antenna.classList.toggle('active');
this.updateCSIDisplay();
});
});
}
// Start CSI simulation
startCSISimulation() {
// Initial update
this.updateCSIDisplay();
// Set up periodic updates
this.csiUpdateInterval = setInterval(() => {
if (this.hasActiveAntennas()) {
this.updateCSIDisplay();
}
}, 1000);
}
// Check if any antennas are active
hasActiveAntennas() {
return this.antennas.some(antenna => antenna.classList.contains('active'));
}
// Update CSI display
updateCSIDisplay() {
const activeAntennas = this.antennas.filter(a => a.classList.contains('active'));
const isActive = activeAntennas.length > 0;
// Get display elements
const amplitudeFill = this.container.querySelector('.csi-fill.amplitude');
const phaseFill = this.container.querySelector('.csi-fill.phase');
const amplitudeValue = this.container.querySelector('.csi-row:first-child .csi-value');
const phaseValue = this.container.querySelector('.csi-row:last-child .csi-value');
if (!isActive) {
// Set to zero when no antennas active
if (amplitudeFill) amplitudeFill.style.width = '0%';
if (phaseFill) phaseFill.style.width = '0%';
if (amplitudeValue) amplitudeValue.textContent = '0.00';
if (phaseValue) phaseValue.textContent = '0.0π';
return;
}
// Generate realistic CSI values based on active antennas
const txCount = activeAntennas.filter(a => a.classList.contains('tx')).length;
const rxCount = activeAntennas.filter(a => a.classList.contains('rx')).length;
// Amplitude increases with more active antennas
const baseAmplitude = 0.3 + (txCount * 0.1) + (rxCount * 0.05);
const amplitude = Math.min(0.95, baseAmplitude + (Math.random() * 0.1 - 0.05));
// Phase varies more with multiple antennas
const phaseVariation = 0.5 + (activeAntennas.length * 0.1);
const phase = 0.5 + Math.random() * phaseVariation;
// Update display
if (amplitudeFill) {
amplitudeFill.style.width = `${amplitude * 100}%`;
amplitudeFill.style.transition = 'width 0.5s ease';
}
if (phaseFill) {
phaseFill.style.width = `${phase * 50}%`;
phaseFill.style.transition = 'width 0.5s ease';
}
if (amplitudeValue) {
amplitudeValue.textContent = amplitude.toFixed(2);
}
if (phaseValue) {
phaseValue.textContent = `${phase.toFixed(1)}π`;
}
// Update antenna array visualization
this.updateAntennaArray(activeAntennas);
}
// Update antenna array visualization
updateAntennaArray(activeAntennas) {
const arrayStatus = this.container.querySelector('.array-status');
if (!arrayStatus) return;
const txActive = activeAntennas.filter(a => a.classList.contains('tx')).length;
const rxActive = activeAntennas.filter(a => a.classList.contains('rx')).length;
arrayStatus.innerHTML = `
<div class="array-info">
<span class="info-label">Active TX:</span>
<span class="info-value">${txActive}/3</span>
</div>
<div class="array-info">
<span class="info-label">Active RX:</span>
<span class="info-value">${rxActive}/6</span>
</div>
<div class="array-info">
<span class="info-label">Signal Quality:</span>
<span class="info-value">${this.calculateSignalQuality(txActive, rxActive)}%</span>
</div>
`;
}
// Calculate signal quality based on active antennas
calculateSignalQuality(txCount, rxCount) {
if (txCount === 0 || rxCount === 0) return 0;
const txRatio = txCount / 3;
const rxRatio = rxCount / 6;
const quality = (txRatio * 0.4 + rxRatio * 0.6) * 100;
return Math.round(quality);
}
// Toggle all antennas
toggleAllAntennas(active) {
this.antennas.forEach(antenna => {
antenna.classList.toggle('active', active);
});
this.updateCSIDisplay();
}
// Reset antenna configuration
resetAntennas() {
// Set default configuration (all active)
this.antennas.forEach(antenna => {
antenna.classList.add('active');
});
this.updateCSIDisplay();
}
// Clean up
dispose() {
if (this.csiUpdateInterval) {
clearInterval(this.csiUpdateInterval);
this.csiUpdateInterval = null;
}
this.antennas.forEach(antenna => {
antenna.removeEventListener('click', this.toggleAntenna);
});
}
}

View File

@@ -0,0 +1,391 @@
// Live Demo Tab Component
import { poseService } from '../services/pose.service.js';
import { streamService } from '../services/stream.service.js';
export class LiveDemoTab {
constructor(containerElement) {
this.container = containerElement;
this.isRunning = false;
this.streamConnection = null;
this.poseSubscription = null;
this.signalCanvas = null;
this.poseCanvas = null;
this.signalCtx = null;
this.poseCtx = null;
this.animationFrame = null;
this.signalTime = 0;
this.poseData = null;
}
// Initialize component
init() {
this.setupCanvases();
this.setupControls();
this.initializeDisplays();
}
// Set up canvases
setupCanvases() {
this.signalCanvas = this.container.querySelector('#signalCanvas');
this.poseCanvas = this.container.querySelector('#poseCanvas');
if (this.signalCanvas) {
this.signalCtx = this.signalCanvas.getContext('2d');
}
if (this.poseCanvas) {
this.poseCtx = this.poseCanvas.getContext('2d');
}
}
// Set up control buttons
setupControls() {
const startButton = this.container.querySelector('#startDemo');
const stopButton = this.container.querySelector('#stopDemo');
if (startButton) {
startButton.addEventListener('click', () => this.startDemo());
}
if (stopButton) {
stopButton.addEventListener('click', () => this.stopDemo());
}
}
// Initialize displays
initializeDisplays() {
// Initialize signal canvas
if (this.signalCtx) {
this.signalCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
this.signalCtx.fillRect(0, 0, this.signalCanvas.width, this.signalCanvas.height);
}
// Initialize pose canvas
if (this.poseCtx) {
this.poseCtx.fillStyle = 'rgba(0, 0, 0, 0.2)';
this.poseCtx.fillRect(0, 0, this.poseCanvas.width, this.poseCanvas.height);
}
}
// Start demo
async startDemo() {
if (this.isRunning) return;
try {
// Update UI
this.isRunning = true;
this.updateControls();
this.updateStatus('Starting...', 'info');
// Check stream status
const streamStatus = await streamService.getStatus();
if (!streamStatus.is_active) {
// Try to start streaming
await streamService.start();
}
// Start pose stream
this.streamConnection = poseService.startPoseStream({
minConfidence: 0.5,
maxFps: 30
});
// Subscribe to pose updates
this.poseSubscription = poseService.subscribeToPoseUpdates(update => {
this.handlePoseUpdate(update);
});
// Start animations
this.startAnimations();
// Update status
this.updateStatus('Running', 'success');
} catch (error) {
console.error('Failed to start demo:', error);
this.updateStatus('Failed to start', 'error');
this.stopDemo();
}
}
// Stop demo
stopDemo() {
if (!this.isRunning) return;
// Update UI
this.isRunning = false;
this.updateControls();
this.updateStatus('Stopped', 'info');
// Stop pose stream
if (this.poseSubscription) {
this.poseSubscription();
this.poseSubscription = null;
}
poseService.stopPoseStream();
this.streamConnection = null;
// Stop animations
if (this.animationFrame) {
cancelAnimationFrame(this.animationFrame);
this.animationFrame = null;
}
}
// Update controls
updateControls() {
const startButton = this.container.querySelector('#startDemo');
const stopButton = this.container.querySelector('#stopDemo');
if (startButton) {
startButton.disabled = this.isRunning;
}
if (stopButton) {
stopButton.disabled = !this.isRunning;
}
}
// Update status display
updateStatus(text, type) {
const statusElement = this.container.querySelector('#demoStatus');
if (statusElement) {
statusElement.textContent = text;
statusElement.className = `status status--${type}`;
}
}
// Handle pose updates
handlePoseUpdate(update) {
switch (update.type) {
case 'connected':
console.log('Pose stream connected');
break;
case 'pose_update':
this.poseData = update.data;
this.updateMetrics(update.data);
break;
case 'error':
console.error('Pose stream error:', update.error);
this.updateStatus('Stream error', 'error');
break;
case 'disconnected':
console.log('Pose stream disconnected');
if (this.isRunning) {
this.updateStatus('Disconnected', 'warning');
}
break;
}
}
// Update metrics display
updateMetrics(poseData) {
if (!poseData) return;
// Update signal strength (simulated based on detection confidence)
const signalStrength = this.container.querySelector('#signalStrength');
if (signalStrength) {
const strength = poseData.persons?.length > 0
? -45 - Math.random() * 10
: -55 - Math.random() * 10;
signalStrength.textContent = `${strength.toFixed(0)} dBm`;
}
// Update latency
const latency = this.container.querySelector('#latency');
if (latency && poseData.processing_time) {
latency.textContent = `${poseData.processing_time.toFixed(0)} ms`;
}
// Update person count
const personCount = this.container.querySelector('#personCount');
if (personCount) {
personCount.textContent = poseData.persons?.length || 0;
}
// Update confidence
const confidence = this.container.querySelector('#confidence');
if (confidence && poseData.persons?.length > 0) {
const avgConfidence = poseData.persons.reduce((sum, p) => sum + p.confidence, 0)
/ poseData.persons.length * 100;
confidence.textContent = `${avgConfidence.toFixed(1)}%`;
}
// Update keypoints
const keypoints = this.container.querySelector('#keypoints');
if (keypoints && poseData.persons?.length > 0) {
const totalKeypoints = poseData.persons[0].keypoints?.length || 0;
const detectedKeypoints = poseData.persons[0].keypoints?.filter(kp => kp.confidence > 0.5).length || 0;
keypoints.textContent = `${detectedKeypoints}/${totalKeypoints}`;
}
}
// Start animations
startAnimations() {
const animate = () => {
if (!this.isRunning) return;
// Update signal visualization
this.updateSignalVisualization();
// Update pose visualization
this.updatePoseVisualization();
this.animationFrame = requestAnimationFrame(animate);
};
animate();
}
// Update signal visualization
updateSignalVisualization() {
if (!this.signalCtx) return;
const ctx = this.signalCtx;
const width = this.signalCanvas.width;
const height = this.signalCanvas.height;
// Clear canvas with fade effect
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fillRect(0, 0, width, height);
// Draw amplitude signal
ctx.beginPath();
ctx.strokeStyle = '#1FB8CD';
ctx.lineWidth = 2;
for (let x = 0; x < width; x++) {
const hasData = this.poseData?.persons?.length > 0;
const amplitude = hasData ? 30 : 10;
const frequency = hasData ? 0.05 : 0.02;
const y = height / 2 +
Math.sin(x * frequency + this.signalTime) * amplitude +
Math.sin(x * 0.02 + this.signalTime * 1.5) * 15;
if (x === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
// Draw phase signal
ctx.beginPath();
ctx.strokeStyle = '#FFC185';
ctx.lineWidth = 2;
for (let x = 0; x < width; x++) {
const hasData = this.poseData?.persons?.length > 0;
const amplitude = hasData ? 25 : 15;
const y = height / 2 +
Math.cos(x * 0.03 + this.signalTime * 0.8) * amplitude +
Math.cos(x * 0.01 + this.signalTime * 0.5) * 20;
if (x === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.stroke();
this.signalTime += 0.05;
}
// Update pose visualization
updatePoseVisualization() {
if (!this.poseCtx || !this.poseData) return;
const ctx = this.poseCtx;
const width = this.poseCanvas.width;
const height = this.poseCanvas.height;
// Clear canvas
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
ctx.fillRect(0, 0, width, height);
// Draw each detected person
if (this.poseData.persons) {
this.poseData.persons.forEach((person, index) => {
this.drawPerson(ctx, person, index);
});
}
}
// Draw a person's pose
drawPerson(ctx, person, index) {
if (!person.keypoints) return;
// Define COCO keypoint connections
const connections = [
[0, 1], [0, 2], [1, 3], [2, 4], // Head
[5, 6], [5, 7], [7, 9], [6, 8], [8, 10], // Arms
[5, 11], [6, 12], [11, 12], // Body
[11, 13], [13, 15], [12, 14], [14, 16] // Legs
];
// Scale keypoints to canvas
const scale = Math.min(this.poseCanvas.width, this.poseCanvas.height) / 2;
const offsetX = this.poseCanvas.width / 2;
const offsetY = this.poseCanvas.height / 2;
// Draw skeleton connections
ctx.strokeStyle = `hsl(${index * 60}, 70%, 50%)`;
ctx.lineWidth = 3;
connections.forEach(([i, j]) => {
const kp1 = person.keypoints[i];
const kp2 = person.keypoints[j];
if (kp1 && kp2 && kp1.confidence > 0.3 && kp2.confidence > 0.3) {
ctx.beginPath();
ctx.moveTo(kp1.x * scale + offsetX, kp1.y * scale + offsetY);
ctx.lineTo(kp2.x * scale + offsetX, kp2.y * scale + offsetY);
ctx.stroke();
}
});
// Draw keypoints
ctx.fillStyle = `hsl(${index * 60}, 70%, 60%)`;
person.keypoints.forEach(kp => {
if (kp.confidence > 0.3) {
ctx.beginPath();
ctx.arc(
kp.x * scale + offsetX,
kp.y * scale + offsetY,
5,
0,
Math.PI * 2
);
ctx.fill();
}
});
// Draw confidence label
ctx.fillStyle = 'white';
ctx.font = '12px monospace';
ctx.fillText(
`Person ${index + 1}: ${(person.confidence * 100).toFixed(1)}%`,
10,
20 + index * 20
);
}
// Clean up
dispose() {
this.stopDemo();
}
}

138
ui/components/TabManager.js Normal file
View File

@@ -0,0 +1,138 @@
// Tab Manager Component
export class TabManager {
constructor(containerElement) {
this.container = containerElement;
this.tabs = [];
this.activeTab = null;
this.tabChangeCallbacks = [];
}
// Initialize tabs
init() {
// Find all tabs and contents
this.tabs = Array.from(this.container.querySelectorAll('.nav-tab'));
this.tabContents = Array.from(this.container.querySelectorAll('.tab-content'));
// Set up event listeners
this.tabs.forEach(tab => {
tab.addEventListener('click', () => this.switchTab(tab));
});
// Activate first tab if none active
const activeTab = this.tabs.find(tab => tab.classList.contains('active'));
if (activeTab) {
this.activeTab = activeTab.getAttribute('data-tab');
} else if (this.tabs.length > 0) {
this.switchTab(this.tabs[0]);
}
}
// Switch to a tab
switchTab(tabElement) {
const tabId = tabElement.getAttribute('data-tab');
if (tabId === this.activeTab) {
return;
}
// Update tab states
this.tabs.forEach(tab => {
tab.classList.toggle('active', tab === tabElement);
});
// Update content visibility
this.tabContents.forEach(content => {
content.classList.toggle('active', content.id === tabId);
});
// Update active tab
const previousTab = this.activeTab;
this.activeTab = tabId;
// Notify callbacks
this.notifyTabChange(tabId, previousTab);
}
// Switch to tab by ID
switchToTab(tabId) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
this.switchTab(tab);
}
}
// Register tab change callback
onTabChange(callback) {
this.tabChangeCallbacks.push(callback);
// Return unsubscribe function
return () => {
const index = this.tabChangeCallbacks.indexOf(callback);
if (index > -1) {
this.tabChangeCallbacks.splice(index, 1);
}
};
}
// Notify tab change callbacks
notifyTabChange(newTab, previousTab) {
this.tabChangeCallbacks.forEach(callback => {
try {
callback(newTab, previousTab);
} catch (error) {
console.error('Error in tab change callback:', error);
}
});
}
// Get active tab
getActiveTab() {
return this.activeTab;
}
// Enable/disable tab
setTabEnabled(tabId, enabled) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
tab.disabled = !enabled;
tab.classList.toggle('disabled', !enabled);
}
}
// Show/hide tab
setTabVisible(tabId, visible) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (tab) {
tab.style.display = visible ? '' : 'none';
}
}
// Add badge to tab
setTabBadge(tabId, badge) {
const tab = this.tabs.find(t => t.getAttribute('data-tab') === tabId);
if (!tab) return;
// Remove existing badge
const existingBadge = tab.querySelector('.tab-badge');
if (existingBadge) {
existingBadge.remove();
}
// Add new badge if provided
if (badge) {
const badgeElement = document.createElement('span');
badgeElement.className = 'tab-badge';
badgeElement.textContent = badge;
tab.appendChild(badgeElement);
}
}
// Clean up
dispose() {
this.tabs.forEach(tab => {
tab.removeEventListener('click', this.switchTab);
});
this.tabChangeCallbacks = [];
}
}