diff --git a/THEME_IMPROVEMENTS.md b/THEME_IMPROVEMENTS.md new file mode 100644 index 0000000..5ba6df9 --- /dev/null +++ b/THEME_IMPROVEMENTS.md @@ -0,0 +1,67 @@ +# Theme Improvements - Better Contrast and Readability + +## Issues Fixed + +### 1. **Too Bright Light Theme** +- **Before**: Very bright white backgrounds that were harsh on the eyes +- **After**: Softer, more muted backgrounds using `#f1f5f9`, `#e2e8f0`, and `#cbd5e1` + +### 2. **Poor Text Contrast** +- **Before**: Many text elements had the same color as background, making them invisible +- **After**: Strong contrast with dark text (`#0f172a`, `#1e293b`, `#334155`) on light backgrounds + +### 3. **Specific Text Color Issues Fixed** +- Member hostnames and IPs now use `var(--text-primary)` for maximum readability +- Latency labels and values have proper contrast +- Navigation tabs are clearly visible in both themes +- Form inputs and dropdowns have proper text contrast +- Status indicators have appropriate colors with good contrast + +## Color Palette Updates + +### Light Theme (Improved) +- **Background**: Softer gradients (`#f1f5f9` → `#e2e8f0` → `#cbd5e1`) +- **Text Primary**: `#0f172a` (very dark for maximum contrast) +- **Text Secondary**: `#1e293b` (dark gray) +- **Text Tertiary**: `#334155` (medium gray) +- **Text Muted**: `#475569` (lighter gray) +- **Borders**: Stronger borders (`rgba(30, 41, 59, 0.2)`) for better definition + +### Dark Theme (Unchanged) +- Maintains the original dark theme for users who prefer it +- All existing functionality preserved + +## Technical Improvements + +### CSS Specificity +- Added `!important` rules for critical text elements to ensure they override any conflicting styles +- Used `[data-theme="light"]` selectors for theme-specific overrides + +### Contrast Ratios +- All text now meets WCAG AA contrast requirements +- Status indicators have clear, distinguishable colors +- Interactive elements have proper hover states + +### Readability Enhancements +- Removed problematic opacity values that made text invisible +- Ensured all form elements have proper text contrast +- Fixed dropdown options to be readable +- Improved error and success message visibility + +## Testing + +To test the improvements: + +1. Open `http://localhost:8080` in your browser +2. Click the theme toggle to switch to light theme +3. Verify all text is clearly readable +4. Check that the background is not too bright +5. Test form interactions and dropdowns +6. Verify status indicators are clearly visible + +## Files Modified + +- `public/styles/theme.css` - Updated color palette and added contrast fixes +- `public/styles/main.css` - Fixed text color issues and opacity problems + +The light theme is now much easier on the eyes with proper contrast for all text elements. diff --git a/THEME_README.md b/THEME_README.md new file mode 100644 index 0000000..1e0f36a --- /dev/null +++ b/THEME_README.md @@ -0,0 +1,144 @@ +# SPORE UI Theme System + +This document describes the theme system implementation for SPORE UI, which provides both dark and light themes with a user-friendly theme switcher. + +## Features + +- **Dark Theme (Default)**: The original dark theme with dark backgrounds and light text +- **Light Theme**: A new light theme with light backgrounds and dark text +- **Theme Switcher**: A toggle button in the header to switch between themes +- **Persistence**: Theme preference is saved in localStorage +- **Smooth Transitions**: CSS transitions for smooth theme switching +- **Responsive Design**: Theme switcher adapts to mobile screens + +## Files Added/Modified + +### New Files +- `public/styles/theme.css` - CSS variables and theme definitions +- `public/scripts/theme-manager.js` - JavaScript theme management +- `THEME_README.md` - This documentation + +### Modified Files +- `public/index.html` - Added theme switcher to header and theme CSS link +- `public/styles/main.css` - Updated to use CSS variables instead of hardcoded colors + +## Implementation Details + +### CSS Variables System + +The theme system uses CSS custom properties (variables) defined in `theme.css`: + +```css +:root { + /* Dark theme variables */ + --bg-primary: linear-gradient(135deg, #2c3e50 0%, #34495e 50%, #1a252f 100%); + --text-primary: #ecf0f1; + --border-primary: rgba(255, 255, 255, 0.1); + /* ... more variables */ +} + +[data-theme="light"] { + /* Light theme overrides */ + --bg-primary: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #cbd5e1 100%); + --text-primary: #1e293b; + --border-primary: rgba(30, 41, 59, 0.1); + /* ... more variables */ +} +``` + +### Theme Switcher + +The theme switcher is located in the header's right section and includes: +- A label indicating "Theme:" +- A toggle button with sun/moon icons +- Hover effects and smooth animations +- Mobile-responsive design + +### JavaScript Theme Manager + +The `ThemeManager` class handles: +- Theme persistence in localStorage +- Theme switching logic +- Icon updates (sun for light, moon for dark) +- Event dispatching for other components +- System theme detection (future enhancement) + +## Usage + +### For Users +1. Click the theme toggle button in the header +2. The theme will switch immediately with smooth transitions +3. Your preference is automatically saved + +### For Developers + +#### Accessing Current Theme +```javascript +// Get current theme +const currentTheme = window.themeManager.getCurrentTheme(); + +// Listen for theme changes +window.addEventListener('themeChanged', (event) => { + console.log('Theme changed to:', event.detail.theme); +}); +``` + +#### Programmatic Theme Setting +```javascript +// Set theme programmatically +window.themeManager.setTheme('light'); // or 'dark' +``` + +## Color Palette + +### Dark Theme +- **Background**: Dark gradients (#2c3e50, #34495e, #1a252f) +- **Text**: Light colors (#ecf0f1, rgba(255,255,255,0.8)) +- **Accents**: Bright colors (#4ade80, #60a5fa, #fbbf24) +- **Borders**: Subtle white borders with low opacity + +### Light Theme +- **Background**: Light gradients (#f8fafc, #e2e8f0, #cbd5e1) +- **Text**: Dark colors (#1e293b, rgba(30,41,59,0.8)) +- **Accents**: Muted colors (#059669, #2563eb, #d97706) +- **Borders**: Subtle dark borders with low opacity + +## Browser Support + +- Modern browsers with CSS custom properties support +- localStorage support for theme persistence +- Graceful degradation for older browsers + +## Future Enhancements + +1. **System Theme Detection**: Automatically detect user's system preference +2. **More Themes**: Additional theme options (e.g., high contrast, colorblind-friendly) +3. **Theme Customization**: Allow users to customize accent colors +4. **Animation Preferences**: Respect user's motion preferences + +## Testing + +To test the theme system: + +1. Start the development server: `cd public && python3 -m http.server 8080` +2. Open `http://localhost:8080` in your browser +3. Click the theme toggle button in the header +4. Verify smooth transitions and proper color contrast +5. Refresh the page to verify theme persistence + +## Troubleshooting + +### Theme Not Switching +- Check browser console for JavaScript errors +- Verify `theme-manager.js` is loaded +- Check if localStorage is available and not disabled + +### Colors Not Updating +- Verify `theme.css` is loaded after `main.css` +- Check if CSS variables are supported in your browser +- Ensure the `data-theme` attribute is being set on the `` element + +### Mobile Issues +- Test on actual mobile devices, not just browser dev tools +- Verify touch events are working properly +- Check responsive CSS rules for theme switcher diff --git a/index.js b/index.js index 01a557b..3725c9f 100644 --- a/index.js +++ b/index.js @@ -466,7 +466,7 @@ app.get('/api/node/status', async (req, res) => { }); // Proxy endpoint to get node capabilities (optionally for a specific node via ?ip=) -app.get('/api/capabilities', async (req, res) => { +app.get('/api/node/endpoints', async (req, res) => { try { const { ip } = req.query; @@ -476,9 +476,9 @@ app.get('/api/capabilities', async (req, res) => { const caps = await nodeClient.getCapabilities(); return res.json(caps); } catch (innerError) { - console.error('Error fetching capabilities from specific node:', innerError); + console.error('Error fetching endpoints from specific node:', innerError); return res.status(500).json({ - error: 'Failed to fetch capabilities from node', + error: 'Failed to fetch endpoints from node', message: innerError.message }); } diff --git a/public/index.html b/public/index.html index 045a009..d77de05 100644 --- a/public/index.html +++ b/public/index.html @@ -6,6 +6,7 @@ SPORE UI + @@ -22,6 +23,14 @@ @@ -147,6 +156,7 @@ + diff --git a/public/scripts/api-client.js b/public/scripts/api-client.js index 17e6204..4d3b817 100644 --- a/public/scripts/api-client.js +++ b/public/scripts/api-client.js @@ -80,11 +80,11 @@ class ApiClient { return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined }); } - async getCapabilities(ip) { - return this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined }); + async getEndpoints(ip) { + return this.request('/api/node/endpoints', { method: 'GET', query: ip ? { ip } : undefined }); } - async callCapability({ ip, method, uri, params }) { + async callEndpoint({ ip, method, uri, params }) { return this.request('/api/proxy-call', { method: 'POST', body: { ip, method, uri, params } diff --git a/public/scripts/components/NodeDetailsComponent.js b/public/scripts/components/NodeDetailsComponent.js index 5268ba3..cc23619 100644 --- a/public/scripts/components/NodeDetailsComponent.js +++ b/public/scripts/components/NodeDetailsComponent.js @@ -10,13 +10,13 @@ class NodeDetailsComponent extends Component { this.subscribeToProperty('isLoading', this.handleLoadingUpdate.bind(this)); this.subscribeToProperty('error', this.handleErrorUpdate.bind(this)); this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this)); - this.subscribeToProperty('capabilities', this.handleCapabilitiesUpdate.bind(this)); + this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this)); } // Handle node status update with state preservation handleNodeStatusUpdate(newStatus, previousStatus) { if (newStatus && !this.viewModel.get('isLoading')) { - this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('capabilities')); + this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints')); } } @@ -24,7 +24,7 @@ class NodeDetailsComponent extends Component { handleTasksUpdate(newTasks, previousTasks) { const nodeStatus = this.viewModel.get('nodeStatus'); if (nodeStatus && !this.viewModel.get('isLoading')) { - this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('capabilities')); + this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints')); } } @@ -48,12 +48,12 @@ class NodeDetailsComponent extends Component { this.updateActiveTab(newTab, previousTab); } - // Handle capabilities update with state preservation - handleCapabilitiesUpdate(newCapabilities, previousCapabilities) { + // Handle endpoints update with state preservation + handleEndpointsUpdate(newEndpoints, previousEndpoints) { const nodeStatus = this.viewModel.get('nodeStatus'); const tasks = this.viewModel.get('tasks'); if (nodeStatus && !this.viewModel.get('isLoading')) { - this.renderNodeDetails(nodeStatus, tasks, newCapabilities); + this.renderNodeDetails(nodeStatus, tasks, newEndpoints); } } @@ -62,7 +62,7 @@ class NodeDetailsComponent extends Component { const tasks = this.viewModel.get('tasks'); const isLoading = this.viewModel.get('isLoading'); const error = this.viewModel.get('error'); - const capabilities = this.viewModel.get('capabilities'); + const endpoints = this.viewModel.get('endpoints'); if (isLoading) { this.renderLoading('
Loading detailed information...
'); @@ -79,10 +79,10 @@ class NodeDetailsComponent extends Component { return; } - this.renderNodeDetails(nodeStatus, tasks, capabilities); + this.renderNodeDetails(nodeStatus, tasks, endpoints); } - renderNodeDetails(nodeStatus, tasks, capabilities) { + renderNodeDetails(nodeStatus, tasks, endpoints) { // Use persisted active tab from the view model, default to 'status' const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status'; logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab); @@ -92,7 +92,6 @@ class NodeDetailsComponent extends Component {
-
@@ -121,14 +120,9 @@ class NodeDetailsComponent extends Component {
- ${nodeStatus.api ? nodeStatus.api.map(endpoint => - `
${endpoint.method === 1 ? 'GET' : 'POST'} ${endpoint.uri}
` - ).join('') : '
No API endpoints available
'} + ${this.renderEndpointsTab(endpoints)}
-
- ${this.renderCapabilitiesTab(capabilities)} -
${this.renderTasksTab(tasks)} @@ -150,18 +144,18 @@ class NodeDetailsComponent extends Component { this.setupFirmwareUpload(); } - renderCapabilitiesTab(capabilities) { - if (!capabilities || !Array.isArray(capabilities.endpoints) || capabilities.endpoints.length === 0) { + renderEndpointsTab(endpoints) { + if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) { return ` -
-
🧩 No capabilities reported
-
This node did not return any capabilities
+
+
🧩 No endpoints reported
+
This node did not return any endpoints
`; } // Sort endpoints by URI (name), then by method for stable ordering - const endpoints = [...capabilities.endpoints].sort((a, b) => { + const endpointsList = [...endpoints.endpoints].sort((a, b) => { const aUri = String(a.uri || '').toLowerCase(); const bUri = String(b.uri || '').toLowerCase(); if (aUri < bUri) return -1; @@ -171,22 +165,22 @@ class NodeDetailsComponent extends Component { return aMethod.localeCompare(bMethod); }); - const total = endpoints.length; + const total = endpointsList.length; // Preserve selection based on a stable key of method+uri if available - const selectedKey = String(this.getUIState('capSelectedKey') || ''); - let selectedIndex = endpoints.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey); + const selectedKey = String(this.getUIState('endpointSelectedKey') || ''); + let selectedIndex = endpointsList.findIndex(ep => `${ep.method} ${ep.uri}` === selectedKey); if (selectedIndex === -1) { - selectedIndex = Number(this.getUIState('capSelectedIndex')); + selectedIndex = Number(this.getUIState('endpointSelectedIndex')); if (Number.isNaN(selectedIndex) || selectedIndex < 0 || selectedIndex >= total) { selectedIndex = 0; } } // Compute padding for aligned display in dropdown - const maxMethodLen = endpoints.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0); + const maxMethodLen = endpointsList.reduce((m, ep) => Math.max(m, String(ep.method || '').length), 0); - const selectorOptions = endpoints.map((ep, idx) => { + const selectorOptions = endpointsList.map((ep, idx) => { const method = String(ep.method || ''); const uri = String(ep.uri || ''); const padCount = Math.max(1, (maxMethodLen - method.length) + 2); @@ -194,12 +188,12 @@ class NodeDetailsComponent extends Component { return ``; }).join(''); - const items = endpoints.map((ep, idx) => { - const formId = `cap-form-${idx}`; - const resultId = `cap-result-${idx}`; + const items = endpointsList.map((ep, idx) => { + const formId = `endpoint-form-${idx}`; + const resultId = `endpoint-result-${idx}`; const params = Array.isArray(ep.params) && ep.params.length > 0 - ? `
${ep.params.map((p, pidx) => ` -