Add documentation and utility modules

- Add API specification documentation
- Add system specification document
- Add UI mockups and documentation
- Add utility modules (wifi)
This commit is contained in:
2026-01-11 21:34:18 +13:00
parent 5f6e45af09
commit 9e2409430c
26 changed files with 7374 additions and 0 deletions

View File

@@ -0,0 +1,239 @@
# Custom Color Picker Component
A cross-platform, cross-browser color picker component that provides a consistent user experience across all operating systems and browsers.
## Features
**Consistent UI** - Same appearance and behavior on Windows, macOS, Linux, iOS, and Android
**Browser Support** - Works in Chrome, Firefox, Safari, Edge, Opera, and mobile browsers
**Touch Support** - Full touch/gesture support for mobile devices
**HSB Color Model** - Uses Hue, Saturation, Brightness for intuitive color selection
**Multiple Input Methods** - Hex input, RGB inputs, and visual picker
**Accessible** - Keyboard accessible and screen reader friendly
**Customizable** - Easy to style and integrate
## Files
- `color-picker.js` - Main JavaScript component (14KB)
- `color-picker.css` - Stylesheet (4KB)
- `color-picker-demo.html` - Demo page showing usage examples
## Quick Start
### 1. Include the files
```html
<link rel="stylesheet" href="color-picker.css">
<script src="color-picker.js"></script>
```
### 2. Create a container element
```html
<div id="my-color-picker"></div>
```
### 3. Initialize the color picker
```javascript
const picker = new ColorPicker('#my-color-picker', {
initialColor: '#FF0000',
onColorChange: (color) => {
console.log('Color changed to:', color);
}
});
```
## API
### Constructor
```javascript
new ColorPicker(container, options)
```
**Parameters:**
- `container` (string|HTMLElement) - CSS selector or DOM element
- `options` (object) - Configuration options
**Options:**
- `initialColor` (string) - Initial color in hex format (default: '#FF0000')
- `onColorChange` (function) - Callback when color changes (receives hex color string)
- `showHexInput` (boolean) - Show hex input field (default: true)
### Methods
```javascript
// Get current color
const color = picker.getColor(); // Returns hex string like '#FF0000'
// Set color programmatically
picker.setColor('#00FF00');
// Open the picker panel
picker.open();
// Close the picker panel
picker.close();
// Toggle the picker panel
picker.toggle();
```
## Usage Examples
### Basic Usage
```javascript
const picker = new ColorPicker('#picker1', {
initialColor: '#FF0000'
});
```
### With Callback
```javascript
const picker = new ColorPicker('#picker1', {
initialColor: '#FF0000',
onColorChange: (color) => {
document.body.style.backgroundColor = color;
}
});
```
### Multiple Color Pickers
```javascript
const colors = ['#FF0000', '#00FF00', '#0000FF'];
const pickers = colors.map((color, index) => {
return new ColorPicker(`#picker-${index}`, {
initialColor: color,
onColorChange: (newColor) => {
colors[index] = newColor;
updateLEDColors(colors);
}
});
});
```
### Dynamic Color Picker Creation
```javascript
function addColorPicker(containerId, initialColor = '#000000') {
const container = document.createElement('div');
container.id = containerId;
document.getElementById('color-list').appendChild(container);
return new ColorPicker(container, {
initialColor: initialColor,
onColorChange: (color) => {
console.log(`Color ${containerId} changed to ${color}`);
}
});
}
// Add multiple pickers
addColorPicker('color-1', '#FF0000');
addColorPicker('color-2', '#00FF00');
```
## Styling
The color picker uses CSS classes that can be customized:
- `.color-picker-container` - Main container
- `.color-picker-preview` - Color preview button
- `.color-picker-panel` - Dropdown panel
- `.color-picker-main` - Main color area
- `.color-picker-hue` - Hue slider
- `.color-picker-controls` - Controls section
### Custom Styling Example
```css
.color-picker-preview {
width: 80px;
height: 80px;
border-radius: 12px;
}
.color-picker-panel {
background: #2d3748;
border-color: #4a5568;
}
```
## Browser Compatibility
| Browser | Version | Status |
|---------|---------|--------|
| Chrome | 60+ | ✅ Full support |
| Firefox | 55+ | ✅ Full support |
| Safari | 12+ | ✅ Full support |
| Edge | 79+ | ✅ Full support |
| Opera | 47+ | ✅ Full support |
| Mobile Safari | iOS 12+ | ✅ Full support |
| Chrome Mobile | Android 7+ | ✅ Full support |
## Operating System Compatibility
- ✅ Windows 10/11
- ✅ macOS 10.14+
- ✅ Linux (all major distributions)
- ✅ iOS 12+
- ✅ Android 7+
## Color Format
The color picker uses **hex color format** (`#RRGGBB`):
- Always returns uppercase hex strings (e.g., `#FF0000`)
- Accepts both uppercase and lowercase input
- Automatically validates hex format
## Integration with LED Driver Mockups
The color picker is integrated into:
- `dashboard.html` - Color selection for patterns
- `presets.html` - Color selection when creating/editing presets
### Example: Getting Colors from Multiple Pickers
```javascript
const colorPickers = [];
function getSelectedColors() {
return colorPickers.map(picker => picker.getColor());
}
function sendColorsToDevice() {
const colors = getSelectedColors();
// Send to LED device via API
fetch('/api/colors', {
method: 'POST',
body: JSON.stringify({ colors: colors })
});
}
```
## Performance
- Lightweight: ~14KB JavaScript, ~4KB CSS
- Fast rendering: Uses Canvas API for color gradients
- Smooth interactions: Optimized event handling
- Memory efficient: No external dependencies
## Accessibility
- Keyboard navigation support
- ARIA labels on interactive elements
- High contrast cursor indicators
- Screen reader compatible
## License
Part of the LED Driver project. Use freely in your projects.
## Demo
See `color-picker-demo.html` for a live demonstration of the color picker component.

56
docs/mockups/README.md Normal file
View File

@@ -0,0 +1,56 @@
# UI Mockups
This directory contains HTML mockups and generated images for the LED Driver user interface.
## Files
### HTML Mockups
- **index.html** - Navigation page linking to all mockups
- **dashboard.html** - Main control panel for managing LED patterns and devices
- **pattern-selector.html** - Visual pattern selection interface
- **device-management.html** - Device and group management interface
- **settings.html** - Comprehensive settings configuration panel
### Generated Images
Images are automatically generated in the `images/` directory:
- `dashboard.png`
- `pattern-selector.png`
- `device-management.png`
- `settings.png`
- `index.png`
## Generating Images
To generate images from the HTML files, use the provided script:
```bash
# Install dependencies (if not already installed)
pipenv install playwright
pipenv run playwright install chromium
# Generate images
pipenv run python generate_images.py
```
The script will:
1. Check for available screenshot libraries (Playwright, Selenium, or html2image)
2. Generate PNG images from all HTML files
3. Save images to the `images/` directory
### Requirements
The script supports multiple screenshot libraries (in order of preference):
1. **Playwright** (recommended) - `pip install playwright && playwright install chromium`
2. **Selenium** - `pip install selenium` (requires ChromeDriver)
3. **html2image** - `pip install html2image`
## Viewing Mockups
Simply open any HTML file in a web browser to view the mockup. Start with `index.html` for navigation to all mockups.
## Notes
- All mockups are responsive and work on desktop and mobile devices
- The mockups use modern CSS with gradients and smooth animations
- Interactive elements (buttons, sliders, etc.) are functional in the HTML but are mockups (no backend connection)

View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chromium Color Picker Demo</title>
<link rel="stylesheet" href="color-picker-chromium.css">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&family=Roboto+Mono&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
h1 {
color: #202124;
margin-bottom: 8px;
font-weight: 400;
font-size: 24px;
}
p {
color: #5f6368;
margin-bottom: 32px;
font-size: 14px;
}
.demo-section {
margin-bottom: 40px;
padding: 24px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e8eaed;
}
.demo-section h2 {
color: #202124;
margin-bottom: 16px;
font-size: 16px;
font-weight: 500;
}
.color-pickers {
display: flex;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.color-display {
margin-top: 16px;
padding: 12px;
background: white;
border-radius: 6px;
font-family: 'Roboto Mono', 'Courier New', monospace;
font-size: 13px;
border: 1px solid #e8eaed;
}
.color-display strong {
color: #4285f4;
}
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-top: 24px;
}
.comparison-item {
padding: 16px;
background: white;
border-radius: 6px;
border: 1px solid #e8eaed;
}
.comparison-item h3 {
font-size: 14px;
font-weight: 500;
color: #202124;
margin-bottom: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>Chromium-style Color Picker</h1>
<p>Color picker that matches the native Chromium browser color picker design</p>
<div class="demo-section">
<h2>Single Color Picker</h2>
<div class="color-pickers">
<div id="picker1"></div>
</div>
<div class="color-display">
Selected color: <strong id="color1-display">#FF0000</strong>
</div>
</div>
<div class="demo-section">
<h2>Multiple Color Pickers</h2>
<p style="margin-bottom: 16px; color: #5f6368; font-size: 14px;">Example: Multiple colors for LED patterns</p>
<div class="color-pickers">
<div id="picker2"></div>
<div id="picker3"></div>
<div id="picker4"></div>
</div>
<div class="color-display">
Colors: <strong id="colors-display">#FF0000, #00FF00, #0000FF</strong>
</div>
</div>
<div class="demo-section">
<h2>Features</h2>
<ul style="color: #5f6368; line-height: 1.8; font-size: 14px;">
<li>✅ Matches native Chromium browser color picker design</li>
<li>✅ Clean, minimal interface with native system fonts</li>
<li>✅ RGB number inputs (no sliders) - Chromium style</li>
<li>✅ Hex input with uppercase formatting</li>
<li>✅ HSB (Hue, Saturation, Brightness) color model</li>
<li>✅ Touch support for mobile devices</li>
<li>✅ Keyboard accessible</li>
<li>✅ Dark mode support</li>
</ul>
</div>
<div class="demo-section">
<h2>Design Notes</h2>
<div class="comparison">
<div class="comparison-item">
<h3>Chromium Style</h3>
<ul style="color: #5f6368; line-height: 1.8; font-size: 13px; list-style: none; padding-left: 0;">
<li>• RGB number inputs only</li>
<li>• Compact preview button</li>
<li>• Native system fonts</li>
<li>• Minimal borders and shadows</li>
<li>• Chromium color scheme</li>
</ul>
</div>
<div class="comparison-item">
<h3>Standard Style</h3>
<ul style="color: #5f6368; line-height: 1.8; font-size: 13px; list-style: none; padding-left: 0;">
<li>• RGB sliders + inputs</li>
<li>• Larger preview button</li>
<li>• Custom styling</li>
<li>• Enhanced shadows</li>
<li>• Custom color scheme</li>
</ul>
</div>
</div>
</div>
</div>
<script src="color-picker-chromium.js"></script>
<script>
// Initialize Chromium-style color pickers
const picker1 = new ColorPickerChromium('#picker1', {
initialColor: '#FF0000',
onColorChange: (color) => {
document.getElementById('color1-display').textContent = color;
}
});
const picker2 = new ColorPickerChromium('#picker2', {
initialColor: '#FF0000',
onColorChange: updateColors
});
const picker3 = new ColorPickerChromium('#picker3', {
initialColor: '#00FF00',
onColorChange: updateColors
});
const picker4 = new ColorPickerChromium('#picker4', {
initialColor: '#0000FF',
onColorChange: updateColors
});
function updateColors() {
const colors = [
picker2.getColor(),
picker3.getColor(),
picker4.getColor()
];
document.getElementById('colors-display').textContent = colors.join(', ');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,253 @@
/* Chromium-style Color Picker - Matches native browser color picker dialog */
.color-picker-container {
position: relative;
display: inline-block;
}
/* Preview button - opens the picker */
.color-picker-preview {
width: 40px;
height: 32px;
border: 1px solid #d0d0d0;
border-radius: 4px;
cursor: pointer;
padding: 0;
background: none;
transition: border-color 0.15s;
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);
}
.color-picker-preview:hover {
border-color: #8ab4f8;
}
.color-picker-preview:active {
border-color: #4285f4;
}
/* Main picker panel - always visible when open, styled like Chromium dialog */
.color-picker-panel {
position: absolute;
top: calc(100% + 4px);
left: 0;
z-index: 1000;
background: #ffffff;
border: 1px solid #dadce0;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15), 0 4px 16px rgba(0, 0, 0, 0.1);
padding: 16px;
min-width: 260px;
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-size: 13px;
}
/* Color area - main saturation/brightness square + hue slider */
.color-picker-area {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
/* Main color square - saturation (left-right) and brightness (top-bottom) */
.color-picker-main {
position: relative;
width: 200px;
height: 200px;
border: 1px solid #dadce0;
border-radius: 4px;
overflow: hidden;
cursor: crosshair;
touch-action: none;
background: #ffffff;
flex-shrink: 0;
}
.color-picker-canvas {
display: block;
width: 100%;
height: 100%;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
/* Cursor for main color area */
.color-picker-cursor {
position: absolute;
width: 18px;
height: 18px;
border: 2px solid #ffffff;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1);
pointer-events: none;
transform: translate(-50%, -50%);
top: 0;
left: 0;
}
/* Hue slider - vertical strip on the right */
.color-picker-hue {
position: relative;
width: 24px;
height: 200px;
border: 1px solid #dadce0;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
touch-action: none;
background: #ffffff;
flex-shrink: 0;
}
/* Hue slider cursor/indicator */
.color-picker-hue-cursor {
position: absolute;
left: 0;
right: 0;
width: 100%;
height: 6px;
border: 2px solid #ffffff;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2), 0 1px 2px rgba(0, 0, 0, 0.1);
pointer-events: none;
transform: translateY(-50%);
top: 0;
}
/* Controls section - hex and RGB inputs */
.color-picker-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
/* Hex input field */
.color-picker-hex {
width: 100%;
padding: 7px 10px;
border: 1px solid #dadce0;
border-radius: 4px;
font-family: 'Roboto Mono', 'Courier New', monospace;
font-size: 13px;
text-transform: uppercase;
transition: border-color 0.15s, box-shadow 0.15s;
background: #ffffff;
}
.color-picker-hex:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
/* RGB inputs container */
.color-picker-rgb {
display: flex;
gap: 12px;
align-items: flex-end;
}
.color-picker-rgb-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.color-picker-rgb-item label {
font-size: 11px;
font-weight: 500;
color: #5f6368;
text-align: left;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* RGB number input fields */
.color-picker-rgb-input {
width: 100%;
padding: 7px 10px;
border: 1px solid #dadce0;
border-radius: 4px;
font-size: 13px;
text-align: left;
transition: border-color 0.15s, box-shadow 0.15s;
background: #ffffff;
font-family: 'Roboto', sans-serif;
-moz-appearance: textfield;
}
.color-picker-rgb-input::-webkit-outer-spin-button,
.color-picker-rgb-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.color-picker-rgb-input:focus {
outline: none;
border-color: #4285f4;
box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
}
/* Hide RGB sliders - Chromium uses only number inputs */
.color-picker-rgb-slider {
display: none;
}
/* Responsive adjustments */
@media (max-width: 480px) {
.color-picker-panel {
left: auto;
right: 0;
}
.color-picker-main {
width: 180px;
height: 180px;
}
.color-picker-hue {
height: 180px;
}
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.color-picker-panel {
background: #202124;
border-color: #5f6368;
}
.color-picker-preview {
border-color: #5f6368;
}
.color-picker-main,
.color-picker-hue {
border-color: #5f6368;
background: #202124;
}
.color-picker-hex,
.color-picker-rgb-input {
background: #303134;
border-color: #5f6368;
color: #e8eaed;
}
.color-picker-rgb-item label {
color: #9aa0a6;
}
.color-picker-hex:focus,
.color-picker-rgb-input:focus {
border-color: #8ab4f8;
box-shadow: 0 0 0 2px rgba(138, 180, 248, 0.2);
}
.color-picker-preview:hover {
border-color: #8ab4f8;
}
}

View File

@@ -0,0 +1,452 @@
/**
* Chromium-style Color Picker Component
* Matches native Chromium browser color picker design
*/
class ColorPickerChromium {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
initialColor: options.initialColor || '#FF0000',
onColorChange: options.onColorChange || null,
showHexInput: options.showHexInput !== false,
...options
};
this.currentColor = this.options.initialColor;
this.isOpen = false;
this.init();
}
init() {
this.createPicker();
this.setupEventListeners();
this.updateColor(this.options.initialColor);
}
createPicker() {
this.container.innerHTML = '';
this.container.className = 'color-picker-container';
// Color preview button
this.previewBtn = document.createElement('button');
this.previewBtn.className = 'color-picker-preview';
this.previewBtn.type = 'button';
this.previewBtn.style.backgroundColor = this.currentColor;
this.previewBtn.setAttribute('aria-label', 'Open color picker');
// Dropdown panel
this.panel = document.createElement('div');
this.panel.className = 'color-picker-panel';
this.panel.style.display = 'none';
// Main color area (hue/saturation)
this.mainArea = document.createElement('div');
this.mainArea.className = 'color-picker-main';
this.mainCanvas = document.createElement('canvas');
this.mainCanvas.width = 200;
this.mainCanvas.height = 200;
this.mainCanvas.className = 'color-picker-canvas';
this.mainArea.appendChild(this.mainCanvas);
// Main area cursor
this.mainCursor = document.createElement('div');
this.mainCursor.className = 'color-picker-cursor';
this.mainArea.appendChild(this.mainCursor);
// Hue slider
this.hueArea = document.createElement('div');
this.hueArea.className = 'color-picker-hue';
this.hueCanvas = document.createElement('canvas');
this.hueCanvas.width = 24;
this.hueCanvas.height = 200;
this.hueCanvas.className = 'color-picker-canvas';
this.hueArea.appendChild(this.hueCanvas);
// Hue slider cursor
this.hueCursor = document.createElement('div');
this.hueCursor.className = 'color-picker-hue-cursor';
this.hueArea.appendChild(this.hueCursor);
// Controls section
this.controls = document.createElement('div');
this.controls.className = 'color-picker-controls';
// Hex input
if (this.options.showHexInput) {
this.hexInput = document.createElement('input');
this.hexInput.type = 'text';
this.hexInput.className = 'color-picker-hex';
this.hexInput.placeholder = '#000000';
this.hexInput.maxLength = 7;
this.controls.appendChild(this.hexInput);
}
// RGB inputs (Chromium style - no sliders, just number inputs)
this.rgbContainer = document.createElement('div');
this.rgbContainer.className = 'color-picker-rgb';
['R', 'G', 'B'].forEach((label) => {
const wrapper = document.createElement('div');
wrapper.className = 'color-picker-rgb-item';
wrapper.dataset.channel = label.toLowerCase();
const labelEl = document.createElement('label');
labelEl.textContent = label;
const input = document.createElement('input');
input.type = 'number';
input.className = 'color-picker-rgb-input';
input.min = 0;
input.max = 255;
input.value = 0;
input.dataset.channel = label.toLowerCase();
wrapper.appendChild(labelEl);
wrapper.appendChild(input);
this.rgbContainer.appendChild(wrapper);
this[`rgb${label}`] = input;
});
this.controls.appendChild(this.rgbContainer);
// Assemble panel
const pickerArea = document.createElement('div');
pickerArea.className = 'color-picker-area';
pickerArea.appendChild(this.mainArea);
pickerArea.appendChild(this.hueArea);
this.panel.appendChild(pickerArea);
this.panel.appendChild(this.controls);
// Assemble container
this.container.appendChild(this.previewBtn);
this.container.appendChild(this.panel);
// Draw canvases
this.drawHueCanvas();
this.drawMainCanvas(1.0); // Start with full saturation
}
setupEventListeners() {
// Toggle panel
this.previewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target) && this.isOpen) {
this.close();
}
});
// Main area interaction
let isMainDragging = false;
this.mainCanvas.addEventListener('mousedown', (e) => {
isMainDragging = true;
this.handleMainAreaClick(e);
});
this.mainCanvas.addEventListener('mousemove', (e) => {
if (isMainDragging) {
this.handleMainAreaClick(e);
}
});
document.addEventListener('mouseup', () => {
isMainDragging = false;
});
// Touch support for main area
this.mainCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isMainDragging = true;
this.handleMainAreaClick(e.touches[0]);
});
this.mainCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isMainDragging) {
this.handleMainAreaClick(e.touches[0]);
}
});
this.mainCanvas.addEventListener('touchend', () => {
isMainDragging = false;
});
// Hue slider interaction
let isHueDragging = false;
this.hueCanvas.addEventListener('mousedown', (e) => {
isHueDragging = true;
this.handleHueClick(e);
});
this.hueCanvas.addEventListener('mousemove', (e) => {
if (isHueDragging) {
this.handleHueClick(e);
}
});
document.addEventListener('mouseup', () => {
isHueDragging = false;
});
// Touch support for hue slider
this.hueCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isHueDragging = true;
this.handleHueClick(e.touches[0]);
});
this.hueCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isHueDragging) {
this.handleHueClick(e.touches[0]);
}
});
this.hueCanvas.addEventListener('touchend', () => {
isHueDragging = false;
});
// Hex input
if (this.hexInput) {
this.hexInput.addEventListener('input', (e) => {
const value = e.target.value;
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
this.updateColor(value);
}
});
this.hexInput.addEventListener('blur', (e) => {
const value = e.target.value;
if (!/^#[0-9A-Fa-f]{6}$/.test(value) && value.length > 0) {
e.target.value = this.currentColor;
}
});
}
// RGB inputs (Chromium style - only number inputs)
['R', 'G', 'B'].forEach(label => {
this[`rgb${label}`].addEventListener('input', (e) => {
let value = parseInt(e.target.value) || 0;
value = Math.max(0, Math.min(255, value)); // Clamp to 0-255
e.target.value = value;
const r = parseInt(this.rgbR.value) || 0;
const g = parseInt(this.rgbG.value) || 0;
const b = parseInt(this.rgbB.value) || 0;
const hex = this.rgbToHex(r, g, b);
this.updateColor(hex, false); // Don't update RGB inputs to avoid loop
});
this[`rgb${label}`].addEventListener('blur', (e) => {
let value = parseInt(e.target.value) || 0;
value = Math.max(0, Math.min(255, value));
e.target.value = value;
});
});
}
drawHueCanvas() {
const ctx = this.hueCanvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
for (let i = 0; i <= 6; i++) {
const hue = i * 60;
gradient.addColorStop(i / 6, `hsl(${hue}, 100%, 50%)`);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 24, 200);
}
drawMainCanvas(hue) {
const ctx = this.mainCanvas.getContext('2d');
// Saturation gradient (left to right)
const satGradient = ctx.createLinearGradient(0, 0, 200, 0);
satGradient.addColorStop(0, `hsl(${hue}, 0%, 50%)`);
satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
ctx.fillStyle = satGradient;
ctx.fillRect(0, 0, 200, 200);
// Brightness gradient (top to bottom)
const brightGradient = ctx.createLinearGradient(0, 0, 0, 200);
brightGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
brightGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
ctx.fillStyle = brightGradient;
ctx.fillRect(0, 0, 200, 200);
}
handleMainAreaClick(e) {
const rect = this.mainCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(200, e.clientX - rect.left));
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const saturation = x / 200;
const brightness = 1 - (y / 200);
this.updateColorFromHSB(this.hue, saturation, brightness);
this.updateCursor(x, y);
}
handleHueClick(e) {
const rect = this.hueCanvas.getBoundingClientRect();
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const hue = (y / 200) * 360;
this.hue = hue;
this.drawMainCanvas(hue);
this.updateHueCursor(y);
// Recalculate color with new hue
const rect2 = this.mainCanvas.getBoundingClientRect();
const x = parseFloat(this.mainCursor.style.left) || 0;
const y2 = parseFloat(this.mainCursor.style.top) || 0;
const saturation = x / 200;
const brightness = 1 - (y2 / 200);
this.updateColorFromHSB(hue, saturation, brightness);
}
updateColorFromHSB(h, s, v) {
const rgb = this.hsbToRgb(h, s, v);
const hex = this.rgbToHex(rgb.r, rgb.g, rgb.b);
this.updateColor(hex);
}
hsbToRgb(h, s, v) {
h = h / 360;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
let r, g, b;
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('').toUpperCase();
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
rgbToHsb(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
if (diff !== 0) {
if (max === r) {
h = ((g - b) / diff) % 6) * 60;
} else if (max === g) {
h = ((b - r) / diff + 2) * 60;
} else {
h = ((r - g) / diff + 4) * 60;
}
}
if (h < 0) h += 360;
const s = max === 0 ? 0 : diff / max;
const v = max;
return { h, s, v };
}
updateColor(hex, updateInputs = true) {
this.currentColor = hex.toUpperCase();
this.previewBtn.style.backgroundColor = this.currentColor;
const rgb = this.hexToRgb(this.currentColor);
if (!rgb) return;
const hsb = this.rgbToHsb(rgb.r, rgb.g, rgb.b);
this.hue = hsb.h;
// Update main canvas
this.drawMainCanvas(this.hue);
// Update cursors
const x = hsb.s * 200;
const y = (1 - hsb.v) * 200;
this.updateCursor(x, y);
this.updateHueCursor((this.hue / 360) * 200);
// Update inputs
if (updateInputs) {
if (this.hexInput) {
this.hexInput.value = this.currentColor;
}
if (this.rgbR) {
this.rgbR.value = rgb.r;
this.rgbG.value = rgb.g;
this.rgbB.value = rgb.b;
}
}
// Callback
if (this.options.onColorChange) {
this.options.onColorChange(this.currentColor);
}
}
updateCursor(x, y) {
this.mainCursor.style.left = `${x}px`;
this.mainCursor.style.top = `${y}px`;
}
updateHueCursor(y) {
this.hueCursor.style.top = `${y}px`;
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
this.panel.style.display = 'block';
this.isOpen = true;
}
close() {
this.panel.style.display = 'none';
this.isOpen = false;
}
getColor() {
return this.currentColor;
}
setColor(color) {
this.updateColor(color);
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = ColorPickerChromium;
}

View File

@@ -0,0 +1,153 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Color Picker Demo - Cross-Platform</title>
<link rel="stylesheet" href="color-picker.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
}
h1 {
color: #667eea;
margin-bottom: 8px;
}
p {
color: #666;
margin-bottom: 32px;
}
.demo-section {
margin-bottom: 40px;
padding: 24px;
background: #f7fafc;
border-radius: 8px;
}
.demo-section h2 {
color: #333;
margin-bottom: 16px;
font-size: 1.25rem;
}
.color-pickers {
display: flex;
gap: 24px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.color-display {
margin-top: 16px;
padding: 12px;
background: white;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
}
.color-display strong {
color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<h1>Custom Color Picker</h1>
<p>Consistent color picker that works the same across all operating systems and browsers</p>
<div class="demo-section">
<h2>Single Color Picker</h2>
<div class="color-pickers">
<div id="picker1"></div>
</div>
<div class="color-display">
Selected color: <strong id="color1-display">#FF0000</strong>
</div>
</div>
<div class="demo-section">
<h2>Multiple Color Pickers</h2>
<p style="margin-bottom: 16px;">Example: Multiple colors for LED patterns</p>
<div class="color-pickers">
<div id="picker2"></div>
<div id="picker3"></div>
<div id="picker4"></div>
</div>
<div class="color-display">
Colors: <strong id="colors-display">#FF0000, #00FF00, #0000FF</strong>
</div>
</div>
<div class="demo-section">
<h2>Features</h2>
<ul style="color: #666; line-height: 1.8;">
<li>✅ Consistent UI across Windows, macOS, Linux, iOS, Android</li>
<li>✅ Works in Chrome, Firefox, Safari, Edge, Opera</li>
<li>✅ Touch support for mobile devices</li>
<li>✅ HSB (Hue, Saturation, Brightness) color model</li>
<li>✅ Hex and RGB input support</li>
<li>✅ Keyboard accessible</li>
<li>✅ Customizable styling</li>
</ul>
</div>
</div>
<script src="color-picker.js"></script>
<script>
// Initialize color pickers
const picker1 = new ColorPicker('#picker1', {
initialColor: '#FF0000',
onColorChange: (color) => {
document.getElementById('color1-display').textContent = color;
}
});
const picker2 = new ColorPicker('#picker2', {
initialColor: '#FF0000',
onColorChange: updateColors
});
const picker3 = new ColorPicker('#picker3', {
initialColor: '#00FF00',
onColorChange: updateColors
});
const picker4 = new ColorPicker('#picker4', {
initialColor: '#0000FF',
onColorChange: updateColors
});
function updateColors() {
const colors = [
picker2.getColor(),
picker3.getColor(),
picker4.getColor()
];
document.getElementById('colors-display').textContent = colors.join(', ');
}
</script>
</body>
</html>

View File

@@ -0,0 +1,282 @@
/* Color Picker Styles - Consistent across all browsers and OS */
.color-picker-container {
position: relative;
display: inline-block;
}
.color-picker-preview {
width: 60px;
height: 60px;
border: 2px solid #e0e0e0;
border-radius: 8px;
cursor: pointer;
padding: 0;
background: none;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.color-picker-preview:hover {
border-color: #667eea;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
.color-picker-preview:active {
transform: translateY(0);
}
.color-picker-panel {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 1000;
background: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
padding: 16px;
min-width: 280px;
}
.color-picker-area {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.color-picker-main {
position: relative;
width: 200px;
height: 200px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: crosshair;
touch-action: none;
}
.color-picker-canvas {
display: block;
width: 100%;
height: 100%;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.color-picker-cursor {
position: absolute;
width: 16px;
height: 16px;
border: 2px solid white;
border-radius: 50%;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
pointer-events: none;
transform: translate(-50%, -50%);
top: 0;
left: 0;
}
.color-picker-hue {
position: relative;
width: 20px;
height: 200px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
touch-action: none;
}
.color-picker-hue-cursor {
position: absolute;
left: 0;
right: 0;
width: 100%;
height: 4px;
border: 2px solid white;
border-radius: 2px;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3);
pointer-events: none;
transform: translateY(-50%);
top: 0;
}
.color-picker-controls {
display: flex;
flex-direction: column;
gap: 12px;
}
.color-picker-hex {
width: 100%;
padding: 8px 12px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
text-transform: uppercase;
transition: border-color 0.2s;
}
.color-picker-hex:focus {
outline: none;
border-color: #667eea;
}
.color-picker-rgb {
display: flex;
gap: 8px;
}
.color-picker-rgb-item {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.color-picker-rgb-item label {
font-size: 0.75rem;
font-weight: 600;
color: #666;
text-align: center;
}
.color-picker-rgb-slider {
width: 100%;
height: 6px;
border-radius: 3px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
cursor: pointer;
}
.color-picker-rgb-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: 2px solid white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
.color-picker-rgb-slider::-webkit-slider-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3);
}
.color-picker-rgb-slider::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: 2px solid white;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.2);
transition: all 0.2s;
}
.color-picker-rgb-slider::-moz-range-thumb:hover {
transform: scale(1.1);
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.3);
}
/* Color-specific slider backgrounds */
.color-picker-rgb-item[data-channel="r"] .color-picker-rgb-slider {
background: linear-gradient(to right, #000000, #ff0000);
}
.color-picker-rgb-item[data-channel="g"] .color-picker-rgb-slider {
background: linear-gradient(to right, #000000, #00ff00);
}
.color-picker-rgb-item[data-channel="b"] .color-picker-rgb-slider {
background: linear-gradient(to right, #000000, #0000ff);
}
.color-picker-rgb-input {
width: 100%;
padding: 6px 8px;
border: 1px solid #e0e0e0;
border-radius: 6px;
font-size: 0.875rem;
text-align: center;
transition: border-color 0.2s;
-moz-appearance: textfield;
}
.color-picker-rgb-input::-webkit-outer-spin-button,
.color-picker-rgb-input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.color-picker-rgb-input:focus {
outline: none;
border-color: #667eea;
}
/* Responsive adjustments */
@media (max-width: 480px) {
.color-picker-panel {
left: auto;
right: 0;
}
.color-picker-main {
width: 180px;
height: 180px;
}
.color-picker-hue {
height: 180px;
}
}
/* Dark mode support (optional) */
@media (prefers-color-scheme: dark) {
.color-picker-panel {
background: #2d3748;
border-color: #4a5568;
}
.color-picker-preview {
border-color: #4a5568;
}
.color-picker-main,
.color-picker-hue {
border-color: #4a5568;
}
.color-picker-hex,
.color-picker-rgb-input {
background: #1a202c;
border-color: #4a5568;
color: #e2e8f0;
}
.color-picker-rgb-item label {
color: #a0aec0;
}
.color-picker-rgb-slider {
background: #4a5568 !important;
}
.color-picker-rgb-slider::-webkit-slider-thumb,
.color-picker-rgb-slider::-moz-range-thumb {
background: #667eea;
border-color: #2d3748;
}
}

View File

@@ -0,0 +1,474 @@
/**
* Custom Color Picker Component
* Consistent across all operating systems and browsers
*/
class ColorPicker {
constructor(container, options = {}) {
this.container = typeof container === 'string' ? document.querySelector(container) : container;
this.options = {
initialColor: options.initialColor || '#FF0000',
onColorChange: options.onColorChange || null,
showHexInput: options.showHexInput !== false,
...options
};
this.currentColor = this.options.initialColor;
this.isOpen = false;
this.init();
}
init() {
this.createPicker();
this.setupEventListeners();
this.updateColor(this.options.initialColor);
}
createPicker() {
this.container.innerHTML = '';
this.container.className = 'color-picker-container';
// Color preview button
this.previewBtn = document.createElement('button');
this.previewBtn.className = 'color-picker-preview';
this.previewBtn.type = 'button';
this.previewBtn.style.backgroundColor = this.currentColor;
this.previewBtn.setAttribute('aria-label', 'Open color picker');
// Dropdown panel
this.panel = document.createElement('div');
this.panel.className = 'color-picker-panel';
this.panel.style.display = 'none';
// Main color area (hue/saturation)
this.mainArea = document.createElement('div');
this.mainArea.className = 'color-picker-main';
this.mainCanvas = document.createElement('canvas');
this.mainCanvas.width = 200;
this.mainCanvas.height = 200;
this.mainCanvas.className = 'color-picker-canvas';
this.mainArea.appendChild(this.mainCanvas);
// Main area cursor
this.mainCursor = document.createElement('div');
this.mainCursor.className = 'color-picker-cursor';
this.mainArea.appendChild(this.mainCursor);
// Hue slider
this.hueArea = document.createElement('div');
this.hueArea.className = 'color-picker-hue';
this.hueCanvas = document.createElement('canvas');
this.hueCanvas.width = 20;
this.hueCanvas.height = 200;
this.hueCanvas.className = 'color-picker-canvas';
this.hueArea.appendChild(this.hueCanvas);
// Hue slider cursor
this.hueCursor = document.createElement('div');
this.hueCursor.className = 'color-picker-hue-cursor';
this.hueArea.appendChild(this.hueCursor);
// Controls section
this.controls = document.createElement('div');
this.controls.className = 'color-picker-controls';
// Hex input
if (this.options.showHexInput) {
this.hexInput = document.createElement('input');
this.hexInput.type = 'text';
this.hexInput.className = 'color-picker-hex';
this.hexInput.placeholder = '#000000';
this.hexInput.maxLength = 7;
this.controls.appendChild(this.hexInput);
}
// RGB inputs and sliders
this.rgbContainer = document.createElement('div');
this.rgbContainer.className = 'color-picker-rgb';
['R', 'G', 'B'].forEach((label, index) => {
const wrapper = document.createElement('div');
wrapper.className = 'color-picker-rgb-item';
wrapper.dataset.channel = label.toLowerCase();
const labelEl = document.createElement('label');
labelEl.textContent = label;
const slider = document.createElement('input');
slider.type = 'range';
slider.className = 'color-picker-rgb-slider';
slider.min = 0;
slider.max = 255;
slider.value = 0;
slider.dataset.channel = label.toLowerCase();
const input = document.createElement('input');
input.type = 'number';
input.className = 'color-picker-rgb-input';
input.min = 0;
input.max = 255;
input.value = 0;
input.dataset.channel = label.toLowerCase();
wrapper.appendChild(labelEl);
wrapper.appendChild(slider);
wrapper.appendChild(input);
this.rgbContainer.appendChild(wrapper);
this[`rgb${label}Slider`] = slider;
this[`rgb${label}`] = input;
});
this.controls.appendChild(this.rgbContainer);
// Assemble panel
const pickerArea = document.createElement('div');
pickerArea.className = 'color-picker-area';
pickerArea.appendChild(this.mainArea);
pickerArea.appendChild(this.hueArea);
this.panel.appendChild(pickerArea);
this.panel.appendChild(this.controls);
// Assemble container
this.container.appendChild(this.previewBtn);
this.container.appendChild(this.panel);
// Draw canvases
this.drawHueCanvas();
this.drawMainCanvas(1.0); // Start with full saturation
}
setupEventListeners() {
// Toggle panel
this.previewBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggle();
});
// Close on outside click
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target) && this.isOpen) {
this.close();
}
});
// Main area interaction
let isMainDragging = false;
this.mainCanvas.addEventListener('mousedown', (e) => {
isMainDragging = true;
this.handleMainAreaClick(e);
});
this.mainCanvas.addEventListener('mousemove', (e) => {
if (isMainDragging) {
this.handleMainAreaClick(e);
}
});
document.addEventListener('mouseup', () => {
isMainDragging = false;
});
// Touch support for main area
this.mainCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isMainDragging = true;
this.handleMainAreaClick(e.touches[0]);
});
this.mainCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isMainDragging) {
this.handleMainAreaClick(e.touches[0]);
}
});
this.mainCanvas.addEventListener('touchend', () => {
isMainDragging = false;
});
// Hue slider interaction
let isHueDragging = false;
this.hueCanvas.addEventListener('mousedown', (e) => {
isHueDragging = true;
this.handleHueClick(e);
});
this.hueCanvas.addEventListener('mousemove', (e) => {
if (isHueDragging) {
this.handleHueClick(e);
}
});
document.addEventListener('mouseup', () => {
isHueDragging = false;
});
// Touch support for hue slider
this.hueCanvas.addEventListener('touchstart', (e) => {
e.preventDefault();
isHueDragging = true;
this.handleHueClick(e.touches[0]);
});
this.hueCanvas.addEventListener('touchmove', (e) => {
e.preventDefault();
if (isHueDragging) {
this.handleHueClick(e.touches[0]);
}
});
this.hueCanvas.addEventListener('touchend', () => {
isHueDragging = false;
});
// Hex input
if (this.hexInput) {
this.hexInput.addEventListener('input', (e) => {
const value = e.target.value;
if (/^#[0-9A-Fa-f]{6}$/.test(value)) {
this.updateColor(value);
}
});
this.hexInput.addEventListener('blur', (e) => {
const value = e.target.value;
if (!/^#[0-9A-Fa-f]{6}$/.test(value) && value.length > 0) {
e.target.value = this.currentColor;
}
});
}
// RGB inputs and sliders
['R', 'G', 'B'].forEach(label => {
// Slider change
this[`rgb${label}Slider`].addEventListener('input', (e) => {
const value = parseInt(e.target.value) || 0;
this[`rgb${label}`].value = value;
const r = parseInt(this.rgbR.value) || 0;
const g = parseInt(this.rgbG.value) || 0;
const b = parseInt(this.rgbB.value) || 0;
const hex = this.rgbToHex(r, g, b);
this.updateColor(hex, false); // Don't update RGB inputs/sliders to avoid loop
});
// Input change
this[`rgb${label}`].addEventListener('input', (e) => {
let value = parseInt(e.target.value) || 0;
value = Math.max(0, Math.min(255, value)); // Clamp to 0-255
e.target.value = value;
this[`rgb${label}Slider`].value = value;
const r = parseInt(this.rgbR.value) || 0;
const g = parseInt(this.rgbG.value) || 0;
const b = parseInt(this.rgbB.value) || 0;
const hex = this.rgbToHex(r, g, b);
this.updateColor(hex, false); // Don't update RGB inputs/sliders to avoid loop
});
});
}
drawHueCanvas() {
const ctx = this.hueCanvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
for (let i = 0; i <= 6; i++) {
const hue = i * 60;
gradient.addColorStop(i / 6, `hsl(${hue}, 100%, 50%)`);
}
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 20, 200);
}
drawMainCanvas(hue) {
const ctx = this.mainCanvas.getContext('2d');
// Saturation gradient (left to right)
const satGradient = ctx.createLinearGradient(0, 0, 200, 0);
satGradient.addColorStop(0, `hsl(${hue}, 0%, 50%)`);
satGradient.addColorStop(1, `hsl(${hue}, 100%, 50%)`);
ctx.fillStyle = satGradient;
ctx.fillRect(0, 0, 200, 200);
// Brightness gradient (top to bottom)
const brightGradient = ctx.createLinearGradient(0, 0, 0, 200);
brightGradient.addColorStop(0, 'rgba(255, 255, 255, 0)');
brightGradient.addColorStop(1, 'rgba(0, 0, 0, 1)');
ctx.fillStyle = brightGradient;
ctx.fillRect(0, 0, 200, 200);
}
handleMainAreaClick(e) {
const rect = this.mainCanvas.getBoundingClientRect();
const x = Math.max(0, Math.min(200, e.clientX - rect.left));
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const saturation = x / 200;
const brightness = 1 - (y / 200);
this.updateColorFromHSB(this.hue, saturation, brightness);
this.updateCursor(x, y);
}
handleHueClick(e) {
const rect = this.hueCanvas.getBoundingClientRect();
const y = Math.max(0, Math.min(200, e.clientY - rect.top));
const hue = (y / 200) * 360;
this.hue = hue;
this.drawMainCanvas(hue);
this.updateHueCursor(y);
// Recalculate color with new hue
const rect2 = this.mainCanvas.getBoundingClientRect();
const x = parseFloat(this.mainCursor.style.left) || 0;
const y2 = parseFloat(this.mainCursor.style.top) || 0;
const saturation = x / 200;
const brightness = 1 - (y2 / 200);
this.updateColorFromHSB(hue, saturation, brightness);
}
updateColorFromHSB(h, s, v) {
const rgb = this.hsbToRgb(h, s, v);
const hex = this.rgbToHex(rgb.r, rgb.g, rgb.b);
this.updateColor(hex);
}
hsbToRgb(h, s, v) {
h = h / 360;
const i = Math.floor(h * 6);
const f = h * 6 - i;
const p = v * (1 - s);
const q = v * (1 - f * s);
const t = v * (1 - (1 - f) * s);
let r, g, b;
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
return {
r: Math.round(r * 255),
g: Math.round(g * 255),
b: Math.round(b * 255)
};
}
rgbToHex(r, g, b) {
return '#' + [r, g, b].map(x => {
const hex = x.toString(16);
return hex.length === 1 ? '0' + hex : hex;
}).join('').toUpperCase();
}
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
} : null;
}
rgbToHsb(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const diff = max - min;
let h = 0;
if (diff !== 0) {
if (max === r) {
h = ((g - b) / diff) % 6) * 60;
} else if (max === g) {
h = ((b - r) / diff + 2) * 60;
} else {
h = ((r - g) / diff + 4) * 60;
}
}
if (h < 0) h += 360;
const s = max === 0 ? 0 : diff / max;
const v = max;
return { h, s, v };
}
updateColor(hex, updateInputs = true) {
this.currentColor = hex.toUpperCase();
this.previewBtn.style.backgroundColor = this.currentColor;
const rgb = this.hexToRgb(this.currentColor);
if (!rgb) return;
const hsb = this.rgbToHsb(rgb.r, rgb.g, rgb.b);
this.hue = hsb.h;
// Update main canvas
this.drawMainCanvas(this.hue);
// Update cursors
const x = hsb.s * 200;
const y = (1 - hsb.v) * 200;
this.updateCursor(x, y);
this.updateHueCursor((this.hue / 360) * 200);
// Update inputs
if (updateInputs) {
if (this.hexInput) {
this.hexInput.value = this.currentColor;
}
if (this.rgbR) {
this.rgbR.value = rgb.r;
this.rgbG.value = rgb.g;
this.rgbB.value = rgb.b;
}
if (this.rgbRSlider) {
this.rgbRSlider.value = rgb.r;
this.rgbGSlider.value = rgb.g;
this.rgbBSlider.value = rgb.b;
}
}
// Callback
if (this.options.onColorChange) {
this.options.onColorChange(this.currentColor);
}
}
updateCursor(x, y) {
this.mainCursor.style.left = `${x}px`;
this.mainCursor.style.top = `${y}px`;
}
updateHueCursor(y) {
this.hueCursor.style.top = `${y}px`;
}
toggle() {
this.isOpen ? this.close() : this.open();
}
open() {
this.panel.style.display = 'block';
this.isOpen = true;
}
close() {
this.panel.style.display = 'none';
this.isOpen = false;
}
getColor() {
return this.currentColor;
}
setColor(color) {
this.updateColor(color);
}
}
// Export for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = ColorPicker;
}

359
docs/mockups/dashboard.html Normal file
View File

@@ -0,0 +1,359 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - Dashboard</title>
<link rel="stylesheet" href="color-picker.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #667eea;
margin-bottom: 8px;
}
.header p {
color: #666;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
}
.card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}
.card h2 {
color: #667eea;
margin-bottom: 16px;
font-size: 1.25rem;
}
.pattern-selector {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 12px;
}
.pattern-btn {
padding: 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: white;
cursor: pointer;
transition: all 0.2s;
text-align: center;
font-weight: 500;
}
.pattern-btn:hover {
border-color: #667eea;
background: #f0f0ff;
}
.pattern-btn.active {
border-color: #667eea;
background: #667eea;
color: white;
}
.slider-group {
margin-bottom: 20px;
}
.slider-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.slider {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
}
.value-display {
display: inline-block;
margin-left: 12px;
font-weight: 600;
color: #667eea;
}
.color-picker-group {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.color-picker-wrapper {
display: inline-block;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
.device-list {
list-style: none;
}
.device-item {
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
align-items: center;
}
.device-status {
width: 12px;
height: 12px;
border-radius: 50%;
background: #4caf50;
}
.device-status.offline {
background: #f44336;
}
.actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.btn-full {
width: 100%;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>LED Driver Control Panel</h1>
<p>Manage your LED devices and patterns</p>
</div>
<div class="grid">
<!-- Pattern Selection -->
<div class="card">
<h2>Pattern Selection</h2>
<div class="pattern-selector">
<div class="pattern-btn active">On</div>
<div class="pattern-btn">Off</div>
<div class="pattern-btn">Blink</div>
<div class="pattern-btn">Chase</div>
<div class="pattern-btn">Circle</div>
<div class="pattern-btn">Pulse</div>
<div class="pattern-btn">Rainbow</div>
<div class="pattern-btn">Transition</div>
</div>
</div>
<!-- Brightness & Speed -->
<div class="card">
<h2>Brightness & Speed</h2>
<div class="slider-group">
<label>
Brightness
<span class="value-display" id="brightness-value">100</span>%
</label>
<input type="range" class="slider" id="brightness" min="0" max="100" value="100">
</div>
<div class="slider-group">
<label>
Delay
<span class="value-display" id="delay-value">100</span>ms
</label>
<input type="range" class="slider" id="delay" min="10" max="1000" value="100" step="10">
</div>
</div>
<!-- Color Selection -->
<div class="card">
<h2>Colors</h2>
<div class="color-picker-group">
<input type="color" class="color-input" value="#000000">
<input type="color" class="color-input" value="#FF0000">
<input type="color" class="color-input" value="#00FF00">
<input type="color" class="color-input" value="#0000FF">
</div>
<div class="actions">
<button class="btn btn-secondary btn-full">Add Color</button>
</div>
</div>
<!-- Device Status -->
<div class="card">
<h2>Connected Devices</h2>
<ul class="device-list">
<li class="device-item">
<div>
<strong>led-device1</strong>
<div style="font-size: 0.875rem; color: #666;">Group: group1</div>
</div>
<div class="device-status"></div>
</li>
<li class="device-item">
<div>
<strong>led-device2</strong>
<div style="font-size: 0.875rem; color: #666;">Group: group2</div>
</div>
<div class="device-status"></div>
</li>
<li class="device-item">
<div>
<strong>led-device3</strong>
<div style="font-size: 0.875rem; color: #666;">No group</div>
</div>
<div class="device-status offline"></div>
</li>
</ul>
</div>
</div>
<!-- Action Buttons -->
<div class="card">
<div class="actions">
<button class="btn btn-primary btn-full">Apply Settings</button>
<button class="btn btn-secondary btn-full">Save to Device</button>
</div>
</div>
</div>
<script>
// Brightness slider
document.getElementById('brightness').addEventListener('input', function(e) {
document.getElementById('brightness-value').textContent = e.target.value;
});
// Delay slider
document.getElementById('delay').addEventListener('input', function(e) {
document.getElementById('delay-value').textContent = e.target.value;
});
// Pattern selection
document.querySelectorAll('.pattern-btn').forEach(btn => {
btn.addEventListener('click', function() {
document.querySelectorAll('.pattern-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
});
});
// Initialize color pickers
const colorPickers = [];
const initialColors = ['#000000', '#FF0000'];
function addColorPicker(color = '#000000') {
const container = document.createElement('div');
container.className = 'color-picker-wrapper';
document.getElementById('color-pickers').appendChild(container);
const picker = new ColorPicker(container, {
initialColor: color,
onColorChange: (newColor) => {
console.log('Color changed:', newColor);
// Update device colors
}
});
colorPickers.push(picker);
return picker;
}
// Add initial color pickers
initialColors.forEach(color => addColorPicker(color));
</script>
<script src="color-picker.js"></script>
</body>
</html>

View File

@@ -0,0 +1,418 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - Device Management</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
background: white;
padding: 8px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.tab {
flex: 1;
padding: 12px 24px;
border: none;
background: transparent;
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s;
}
.tab.active {
background: #667eea;
color: white;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.card h2 {
color: #667eea;
margin-bottom: 20px;
}
.device-item, .group-item {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
display: flex;
justify-content: space-between;
align-items: center;
transition: all 0.2s;
}
.device-item:hover, .group-item:hover {
border-color: #667eea;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.2);
}
.device-info, .group-info {
flex: 1;
}
.device-name, .group-name {
font-weight: 600;
font-size: 1.125rem;
margin-bottom: 4px;
}
.device-details, .group-details {
font-size: 0.875rem;
color: #666;
}
.status-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
margin-right: 12px;
}
.status-online {
background: #d4edda;
color: #155724;
}
.status-offline {
background: #f8d7da;
color: #721c24;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #4caf50;
margin-right: 8px;
}
.status-indicator.offline {
background: #f44336;
}
.device-actions, .group-actions {
display: flex;
gap: 8px;
}
.btn-icon {
padding: 8px 12px;
border: 1px solid #e0e0e0;
background: white;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
}
.btn-icon:hover {
border-color: #667eea;
color: #667eea;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-group input, .form-group select {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
.form-group input:focus, .form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 24px;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
.group-devices {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #e0e0e0;
}
.group-device-tag {
display: inline-block;
padding: 4px 8px;
background: #f0f0f0;
border-radius: 4px;
font-size: 0.75rem;
margin-right: 8px;
margin-top: 8px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Device & Group Management</h1>
<button class="btn btn-primary" onclick="showAddDeviceModal()">+ Add Device</button>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('devices')">Devices</button>
<button class="tab" onclick="switchTab('groups')">Groups</button>
</div>
<!-- Devices Tab -->
<div id="devices-tab" class="tab-content active">
<div class="card">
<h2>Connected Devices</h2>
<div class="device-item">
<div class="device-info">
<div class="device-name">
<span class="status-indicator"></span>
led-device1
</div>
<div class="device-details">
<span class="status-badge status-online">Online</span>
MAC: AA:BB:CC:DD:EE:01 | Group: group1 | Pattern: Rainbow
</div>
</div>
<div class="device-actions">
<button class="btn-icon" title="Edit">✏️</button>
<button class="btn-icon" title="Settings">⚙️</button>
<button class="btn-icon" title="Remove">🗑️</button>
</div>
</div>
<div class="device-item">
<div class="device-info">
<div class="device-name">
<span class="status-indicator"></span>
led-device2
</div>
<div class="device-details">
<span class="status-badge status-online">Online</span>
MAC: AA:BB:CC:DD:EE:02 | Group: group2 | Pattern: Chase
</div>
</div>
<div class="device-actions">
<button class="btn-icon" title="Edit">✏️</button>
<button class="btn-icon" title="Settings">⚙️</button>
<button class="btn-icon" title="Remove">🗑️</button>
</div>
</div>
<div class="device-item">
<div class="device-info">
<div class="device-name">
<span class="status-indicator offline"></span>
led-device3
</div>
<div class="device-details">
<span class="status-badge status-offline">Offline</span>
MAC: AA:BB:CC:DD:EE:03 | No group | Pattern: On
</div>
</div>
<div class="device-actions">
<button class="btn-icon" title="Edit">✏️</button>
<button class="btn-icon" title="Settings">⚙️</button>
<button class="btn-icon" title="Remove">🗑️</button>
</div>
</div>
</div>
</div>
<!-- Groups Tab -->
<div id="groups-tab" class="tab-content">
<div class="card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<h2>Groups</h2>
<button class="btn btn-primary" onclick="showAddGroupModal()">+ Create Group</button>
</div>
<div class="group-item">
<div class="group-info">
<div class="group-name">group1</div>
<div class="group-details">
Pattern: On | Brightness: 100% | Delay: 100ms
</div>
<div class="group-devices">
<span class="group-device-tag">led-device1</span>
</div>
</div>
<div class="group-actions">
<button class="btn-icon" title="Edit">✏️</button>
<button class="btn-icon" title="Apply">▶️</button>
<button class="btn-icon" title="Delete">🗑️</button>
</div>
</div>
<div class="group-item">
<div class="group-info">
<div class="group-name">group2</div>
<div class="group-details">
Pattern: Chase | Brightness: 75% | Delay: 200ms
</div>
<div class="group-devices">
<span class="group-device-tag">led-device2</span>
</div>
</div>
<div class="group-actions">
<button class="btn-icon" title="Edit">✏️</button>
<button class="btn-icon" title="Apply">▶️</button>
<button class="btn-icon" title="Delete">🗑️</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal (simplified) -->
<div id="modal" style="display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center;">
<div class="card" style="max-width: 500px; margin: 20px;">
<h2 id="modal-title">Add Device</h2>
<div class="form-group">
<label>Device Name</label>
<input type="text" id="device-name" placeholder="led-device4">
</div>
<div class="form-group">
<label>MAC Address</label>
<input type="text" id="device-mac" placeholder="AA:BB:CC:DD:EE:04">
</div>
<div class="form-group">
<label>Group</label>
<select id="device-group">
<option value="">No group</option>
<option value="group1">group1</option>
<option value="group2">group2</option>
</select>
</div>
<div class="form-actions">
<button class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button class="btn btn-primary" onclick="saveDevice()">Save</button>
</div>
</div>
</div>
<script>
function switchTab(tab) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tab + '-tab').classList.add('active');
}
function showAddDeviceModal() {
document.getElementById('modal').style.display = 'flex';
document.getElementById('modal-title').textContent = 'Add Device';
}
function showAddGroupModal() {
document.getElementById('modal').style.display = 'flex';
document.getElementById('modal-title').textContent = 'Create Group';
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
function saveDevice() {
alert('Device saved! (This is a mockup)');
closeModal();
}
</script>
</body>
</html>

155
docs/mockups/generate_images.py Executable file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env python3
"""
Generate images from HTML mockup files
Uses Playwright to render HTML and take screenshots
"""
import os
import sys
from pathlib import Path
try:
from playwright.sync_api import sync_playwright
PLAYWRIGHT_AVAILABLE = True
except ImportError:
PLAYWRIGHT_AVAILABLE = False
try:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
SELENIUM_AVAILABLE = True
except ImportError:
SELENIUM_AVAILABLE = False
try:
from html2image import Html2Image
HTML2IMAGE_AVAILABLE = True
except ImportError:
HTML2IMAGE_AVAILABLE = False
def generate_with_playwright(html_file, output_file, width=1920, height=1080):
"""Generate image using Playwright"""
with sync_playwright() as p:
browser = p.chromium.launch(headless=True)
page = browser.new_page(viewport={'width': width, 'height': height})
page.goto(f'file://{html_file.absolute()}')
# Wait for page to load
page.wait_for_timeout(1000)
page.screenshot(path=str(output_file), full_page=True)
browser.close()
print(f"✓ Generated {output_file.name} using Playwright")
def generate_with_selenium(html_file, output_file, width=1920, height=1080):
"""Generate image using Selenium"""
chrome_options = Options()
chrome_options.add_argument('--headless')
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
chrome_options.add_argument(f'--window-size={width},{height}')
driver = webdriver.Chrome(options=chrome_options)
try:
driver.get(f'file://{html_file.absolute()}')
# Wait for page to load
import time
time.sleep(2)
driver.save_screenshot(str(output_file))
print(f"✓ Generated {output_file.name} using Selenium")
finally:
driver.quit()
def generate_with_html2image(html_file, output_file, width=1920, height=1080):
"""Generate image using html2image"""
hti = Html2Image(size=(width, height))
hti.screenshot(
html_file=str(html_file),
save_as=output_file.name,
size=(width, height)
)
print(f"✓ Generated {output_file.name} using html2image")
def generate_image(html_file, output_dir, width=1920, height=1080):
"""Generate image from HTML file using available method"""
html_path = Path(html_file)
output_path = output_dir / f"{html_path.stem}.png"
if PLAYWRIGHT_AVAILABLE:
try:
generate_with_playwright(html_path, output_path, width, height)
return True
except Exception as e:
print(f"Playwright failed: {e}, trying alternatives...")
if SELENIUM_AVAILABLE:
try:
generate_with_selenium(html_path, output_path, width, height)
return True
except Exception as e:
print(f"Selenium failed: {e}, trying alternatives...")
if HTML2IMAGE_AVAILABLE:
try:
generate_with_html2image(html_path, output_path, width, height)
return True
except Exception as e:
print(f"html2image failed: {e}")
return False
def main():
"""Main function to generate images from all HTML files"""
script_dir = Path(__file__).parent
output_dir = script_dir / "images"
output_dir.mkdir(exist_ok=True)
html_files = list(script_dir.glob("*.html"))
if not html_files:
print("No HTML files found in mockups directory")
return
print(f"Found {len(html_files)} HTML file(s)")
print(f"Output directory: {output_dir}")
print()
# Check available libraries
if not any([PLAYWRIGHT_AVAILABLE, SELENIUM_AVAILABLE, HTML2IMAGE_AVAILABLE]):
print("ERROR: No screenshot library available!")
print("\nPlease install one of the following:")
print(" pip install playwright && playwright install chromium")
print(" pip install selenium")
print(" pip install html2image")
sys.exit(1)
print("Available screenshot libraries:")
if PLAYWRIGHT_AVAILABLE:
print(" ✓ Playwright")
if SELENIUM_AVAILABLE:
print(" ✓ Selenium")
if HTML2IMAGE_AVAILABLE:
print(" ✓ html2image")
print()
# Generate images
success_count = 0
for html_file in html_files:
print(f"Generating image from {html_file.name}...")
if generate_image(html_file, output_dir):
success_count += 1
else:
print(f"✗ Failed to generate image from {html_file.name}")
print()
print(f"Successfully generated {success_count}/{len(html_files)} images")
print(f"Images saved to: {output_dir}")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 625 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 600 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 904 KiB

136
docs/mockups/index.html Normal file
View File

@@ -0,0 +1,136 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - UI Mockups</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
display: flex;
align-items: center;
justify-content: center;
}
.container {
max-width: 800px;
width: 100%;
}
.header {
text-align: center;
color: white;
margin-bottom: 40px;
}
.header h1 {
font-size: 3rem;
margin-bottom: 12px;
}
.header p {
font-size: 1.25rem;
opacity: 0.9;
}
.mockups-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 24px;
}
.mockup-card {
background: white;
border-radius: 16px;
padding: 32px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
transition: all 0.3s;
text-decoration: none;
color: inherit;
display: block;
}
.mockup-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.3);
}
.mockup-icon {
font-size: 3rem;
margin-bottom: 16px;
}
.mockup-title {
font-size: 1.5rem;
font-weight: 600;
color: #667eea;
margin-bottom: 8px;
}
.mockup-description {
color: #666;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>LED Driver UI Mockups</h1>
<p>Example user interfaces for the LED driver system</p>
</div>
<div class="mockups-grid">
<a href="dashboard.html" class="mockup-card">
<div class="mockup-icon">📊</div>
<div class="mockup-title">Dashboard</div>
<div class="mockup-description">
Main control panel for managing LED patterns, brightness, colors, and device status.
</div>
</a>
<a href="pattern-selector.html" class="mockup-card">
<div class="mockup-icon">🎨</div>
<div class="mockup-title">Pattern Selector</div>
<div class="mockup-description">
Visual interface for selecting from available LED patterns: On, Off, Blink, Chase, Circle, Pulse, Rainbow, and Transition.
</div>
</a>
<a href="device-management.html" class="mockup-card">
<div class="mockup-icon">🔧</div>
<div class="mockup-title">Device Management</div>
<div class="mockup-description">
Manage connected LED devices and groups. View device status, assign groups, and configure device settings.
</div>
</a>
<a href="settings.html" class="mockup-card">
<div class="mockup-icon">⚙️</div>
<div class="mockup-title">Settings</div>
<div class="mockup-description">
Comprehensive settings panel for configuring LED pin, color order, pattern parameters, and network settings.
</div>
</a>
<a href="presets.html" class="mockup-card">
<div class="mockup-icon">💾</div>
<div class="mockup-title">Presets</div>
<div class="mockup-description">
Save, load, and manage preset configurations with pattern, colors, delay, and all N1-N8 parameters for quick pattern switching.
</div>
</a>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,310 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - Pattern Selector</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 40px;
color: #2d3748;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 8px;
}
.header p {
font-size: 1.125rem;
color: #718096;
}
.patterns-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
}
.pattern-card {
background: white;
border-radius: 16px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
border: 3px solid transparent;
}
.pattern-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.pattern-card.selected {
border-color: #667eea;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.pattern-icon {
width: 100%;
height: 180px;
border-radius: 12px;
margin-bottom: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
background: #f7fafc;
position: relative;
overflow: hidden;
}
.pattern-card.selected .pattern-icon {
background: rgba(255, 255, 255, 0.2);
}
.pattern-name {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 8px;
}
.pattern-description {
font-size: 0.875rem;
color: #718096;
line-height: 1.5;
}
.pattern-card.selected .pattern-description {
color: rgba(255, 255, 255, 0.9);
}
.pattern-preview {
display: flex;
gap: 4px;
margin-top: 12px;
}
.preview-dot {
flex: 1;
height: 8px;
border-radius: 4px;
background: #e2e8f0;
}
.pattern-card.selected .preview-dot {
background: rgba(255, 255, 255, 0.5);
}
.actions {
margin-top: 40px;
text-align: center;
}
.btn {
padding: 16px 48px;
border: none;
border-radius: 12px;
font-size: 1.125rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
margin: 0 12px;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: white;
color: #667eea;
border: 2px solid #667eea;
}
.btn-secondary:hover {
background: #f0f0ff;
}
/* Pattern-specific icons */
.icon-on { background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%); }
.icon-off { background: #2d3748; }
.icon-blink { background: linear-gradient(90deg, #ffd700 25%, #2d3748 25%, #2d3748 50%, #ffd700 50%); }
.icon-chase { background: linear-gradient(90deg, #ff0000 0%, #ff6666 50%, #ff0000 100%); }
.icon-circle { background: radial-gradient(circle, #00ff00 0%, #66ff66 50%, #00ff00 100%); }
.icon-pulse { background: radial-gradient(circle, #0000ff 0%, #6666ff 50%, #0000ff 100%); }
.icon-rainbow { background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3); }
.icon-transition { background: linear-gradient(135deg, #ff0000 0%, #0000ff 100%); }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Select LED Pattern</h1>
<p>Choose a pattern to display on your LED devices</p>
</div>
<div class="patterns-grid">
<div class="pattern-card" data-pattern="on">
<div class="pattern-icon icon-on">💡</div>
<div class="pattern-name">On</div>
<div class="pattern-description">Solid color display - LEDs stay on with selected color</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="off">
<div class="pattern-icon icon-off"></div>
<div class="pattern-name">Off</div>
<div class="pattern-description">Turn all LEDs off</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="blink">
<div class="pattern-icon icon-blink"></div>
<div class="pattern-name">Blink</div>
<div class="pattern-description">All LEDs blink on and off together</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="chase">
<div class="pattern-icon icon-chase">🏃</div>
<div class="pattern-name">Chase</div>
<div class="pattern-description">Light chases along the LED strip</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="circle">
<div class="pattern-icon icon-circle"></div>
<div class="pattern-name">Circle</div>
<div class="pattern-description">Circular pattern that rotates around the strip</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="pulse">
<div class="pattern-icon icon-pulse">💓</div>
<div class="pattern-name">Pulse</div>
<div class="pattern-description">Pulsing effect that fades in and out</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="rainbow">
<div class="pattern-icon icon-rainbow">🌈</div>
<div class="pattern-name">Rainbow</div>
<div class="pattern-description">Smooth rainbow color transition across LEDs</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
<div class="pattern-card" data-pattern="transition">
<div class="pattern-icon icon-transition">🔄</div>
<div class="pattern-name">Transition</div>
<div class="pattern-description">Smooth color transition between selected colors</div>
<div class="pattern-preview">
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
<div class="preview-dot"></div>
</div>
</div>
</div>
<div class="actions">
<button class="btn btn-secondary" onclick="window.history.back()">Cancel</button>
<button class="btn btn-primary" id="apply-btn">Apply Pattern</button>
</div>
</div>
<script>
let selectedPattern = null;
document.querySelectorAll('.pattern-card').forEach(card => {
card.addEventListener('click', function() {
document.querySelectorAll('.pattern-card').forEach(c => c.classList.remove('selected'));
this.classList.add('selected');
selectedPattern = this.dataset.pattern;
});
});
document.getElementById('apply-btn').addEventListener('click', function() {
if (selectedPattern) {
alert(`Applying pattern: ${selectedPattern}`);
// In real implementation, this would send the pattern to the device
} else {
alert('Please select a pattern first');
}
});
</script>
</body>
</html>

968
docs/mockups/presets.html Normal file
View File

@@ -0,0 +1,968 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - Presets</title>
<link rel="stylesheet" href="color-picker.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
color: #667eea;
margin-bottom: 8px;
}
.header p {
color: #666;
}
.controls {
background: white;
border-radius: 12px;
padding: 20px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: center;
}
.search-box {
flex: 1;
min-width: 200px;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
.search-box:focus {
outline: none;
border-color: #667eea;
}
.filter-group {
display: flex;
gap: 12px;
align-items: center;
}
.filter-select {
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
background: white;
cursor: pointer;
}
.filter-select:focus {
outline: none;
border-color: #667eea;
}
.view-toggle {
display: flex;
gap: 8px;
border: 1px solid #e0e0e0;
border-radius: 8px;
overflow: hidden;
}
.view-btn {
padding: 8px 16px;
border: none;
background: white;
cursor: pointer;
transition: all 0.2s;
}
.view-btn.active {
background: #667eea;
color: white;
}
.presets-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 24px;
margin-bottom: 24px;
}
.preset-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
cursor: pointer;
border: 3px solid transparent;
}
.preset-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.preset-card.selected {
border-color: #667eea;
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
}
.preset-header {
display: flex;
justify-content: space-between;
align-items: start;
margin-bottom: 16px;
}
.preset-name {
font-size: 1.5rem;
font-weight: 600;
color: #333;
margin-bottom: 8px;
}
.pattern-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 600;
background: #667eea;
color: white;
margin-bottom: 12px;
}
.pattern-badge.on { background: #4caf50; }
.pattern-badge.off { background: #757575; }
.pattern-badge.blink { background: #ff9800; }
.pattern-badge.chase { background: #f44336; }
.pattern-badge.circle { background: #00bcd4; }
.pattern-badge.pulse { background: #e91e63; }
.pattern-badge.rainbow { background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3); }
.pattern-badge.transition { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
.color-preview {
display: flex;
gap: 8px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.color-swatch {
width: 40px;
height: 40px;
border-radius: 8px;
border: 2px solid #e0e0e0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.preset-info {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 16px;
font-size: 0.875rem;
color: #666;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.info-label {
font-weight: 500;
}
.info-value {
color: #667eea;
font-weight: 600;
}
.preset-actions {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #e0e0e0;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
flex: 1;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-icon {
padding: 8px;
min-width: 40px;
}
.btn-large {
padding: 16px 32px;
font-size: 1rem;
}
/* Modal Styles */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
padding: 20px;
}
.modal.active {
display: flex;
}
.modal-content {
background: white;
border-radius: 12px;
padding: 32px;
max-width: 600px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
}
.modal-header {
margin-bottom: 24px;
}
.modal-header h2 {
color: #667eea;
margin-bottom: 8px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group small {
display: block;
margin-top: 4px;
color: #666;
font-size: 0.875rem;
}
.color-inputs {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.color-input-wrapper {
display: flex;
flex-direction: column;
gap: 8px;
}
.color-input-wrapper {
display: inline-block;
}
.params-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.n-value-input {
width: 100%;
}
.n-value-input:focus {
border-color: #667eea;
outline: none;
}
.param-input {
display: flex;
flex-direction: column;
}
.param-input label {
font-size: 0.75rem;
margin-bottom: 4px;
}
.param-input input {
padding: 8px;
font-size: 0.875rem;
}
.modal-actions {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e0e0e0;
}
.empty-state {
background: white;
border-radius: 12px;
padding: 60px 40px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.empty-state-icon {
font-size: 4rem;
margin-bottom: 16px;
}
.empty-state h2 {
color: #667eea;
margin-bottom: 8px;
}
.empty-state p {
color: #666;
margin-bottom: 24px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div>
<h1>Preset Management</h1>
<p>Save and manage your favorite LED pattern configurations</p>
</div>
<div style="display: flex; gap: 12px; align-items: center;">
<button class="btn btn-secondary btn-large" onclick="syncPresets()" title="Sync all presets to all devices">🔄 Sync Presets to All Devices</button>
<button class="btn btn-primary btn-large" onclick="showCreateModal()">+ Create Preset</button>
</div>
</div>
<div class="controls">
<input type="text" class="search-box" placeholder="Search presets..." id="search-input">
<div class="filter-group">
<select class="filter-select" id="pattern-filter">
<option value="">All Patterns</option>
<option value="on">On</option>
<option value="off">Off</option>
<option value="blink">Blink</option>
<option value="chase">Chase</option>
<option value="circle">Circle</option>
<option value="pulse">Pulse</option>
<option value="rainbow">Rainbow</option>
<option value="transition">Transition</option>
</select>
<select class="filter-select" id="sort-select">
<option value="name">Sort by Name</option>
<option value="recent">Recently Used</option>
<option value="created">Recently Created</option>
</select>
<div class="view-toggle">
<button class="view-btn active" onclick="setView('grid')" id="view-grid">Grid</button>
<button class="view-btn" onclick="setView('list')" id="view-list">List</button>
</div>
</div>
</div>
<div class="presets-grid" id="presets-container">
<!-- Preset Card 1 -->
<div class="preset-card" data-pattern="rainbow" data-name="Fast Rainbow">
<div class="preset-header">
<div>
<div class="preset-name">Fast Rainbow</div>
<span class="pattern-badge rainbow">Rainbow</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #FF0000;"></div>
<div class="color-swatch" style="background: #00FF00;"></div>
<div class="color-swatch" style="background: #0000FF;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">30ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">10</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Fast Rainbow')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Fast Rainbow')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Fast Rainbow')" title="Delete">🗑️</button>
</div>
</div>
<!-- Preset Card 2 -->
<div class="preset-card" data-pattern="pulse" data-name="Slow Pulse">
<div class="preset-header">
<div>
<div class="preset-name">Slow Pulse</div>
<span class="pattern-badge pulse">Pulse</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #FF0000;"></div>
<div class="color-swatch" style="background: #0000FF;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">200ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">500</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Slow Pulse')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Slow Pulse')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Slow Pulse')" title="Delete">🗑️</button>
</div>
</div>
<!-- Preset Card 3 -->
<div class="preset-card" data-pattern="chase" data-name="Red Blue Chase">
<div class="preset-header">
<div>
<div class="preset-name">Red Blue Chase</div>
<span class="pattern-badge chase">Chase</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #FF0000;"></div>
<div class="color-swatch" style="background: #0000FF;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">100ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">5</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Red Blue Chase')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Red Blue Chase')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Red Blue Chase')" title="Delete">🗑️</button>
</div>
</div>
<!-- Preset Card 4 -->
<div class="preset-card" data-pattern="circle" data-name="Loading Circle">
<div class="preset-header">
<div>
<div class="preset-name">Loading Circle</div>
<span class="pattern-badge circle">Circle</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #00FF00;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">50ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">50</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Loading Circle')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Loading Circle')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Loading Circle')" title="Delete">🗑️</button>
</div>
</div>
<!-- Preset Card 5 -->
<div class="preset-card" data-pattern="blink" data-name="Party Blink">
<div class="preset-header">
<div>
<div class="preset-name">Party Blink</div>
<span class="pattern-badge blink">Blink</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #FF00FF;"></div>
<div class="color-swatch" style="background: #00FFFF;"></div>
<div class="color-swatch" style="background: #FFFF00;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">150ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">0</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Party Blink')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Party Blink')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Party Blink')" title="Delete">🗑️</button>
</div>
</div>
<!-- Preset Card 6 -->
<div class="preset-card" data-pattern="transition" data-name="Smooth Transition">
<div class="preset-header">
<div>
<div class="preset-name">Smooth Transition</div>
<span class="pattern-badge transition">Transition</span>
</div>
</div>
<div class="color-preview">
<div class="color-swatch" style="background: #FF0000;"></div>
<div class="color-swatch" style="background: #00FF00;"></div>
<div class="color-swatch" style="background: #0000FF;"></div>
<div class="color-swatch" style="background: #FFFF00;"></div>
</div>
<div class="preset-info">
<div class="info-item">
<span class="info-label">Delay:</span>
<span class="info-value">100ms</span>
</div>
<div class="info-item">
<span class="info-label">N1:</span>
<span class="info-value">0</span>
</div>
</div>
<div class="preset-actions">
<button class="btn btn-primary" onclick="applyPreset('Smooth Transition')">Apply</button>
<button class="btn btn-secondary btn-icon" onclick="editPreset('Smooth Transition')" title="Edit">✏️</button>
<button class="btn btn-danger btn-icon" onclick="deletePreset('Smooth Transition')" title="Delete">🗑️</button>
</div>
</div>
</div>
</div>
<!-- Create/Edit Preset Modal -->
<div class="modal" id="preset-modal">
<div class="modal-content">
<div class="modal-header">
<h2 id="modal-title">Create Preset</h2>
<p>Configure your preset settings</p>
</div>
<form id="preset-form">
<div class="form-group">
<label for="preset-name">Preset Name *</label>
<input type="text" id="preset-name" required placeholder="Enter preset name">
<small>Unique identifier for this preset</small>
</div>
<div class="form-group">
<label for="preset-pattern">Pattern *</label>
<select id="preset-pattern" required>
<option value="on">On</option>
<option value="off">Off</option>
<option value="blink">Blink</option>
<option value="chase">Chase</option>
<option value="circle">Circle</option>
<option value="pulse">Pulse</option>
<option value="rainbow">Rainbow</option>
<option value="transition">Transition</option>
</select>
</div>
<div class="form-group">
<label>Colors *</label>
<div class="color-inputs" id="color-inputs">
<!-- Color pickers will be added here -->
</div>
<button type="button" class="btn btn-secondary" onclick="addColorPicker()" style="margin-top: 8px;">+ Add Color</button>
<small>Minimum 2 colors required</small>
</div>
<div class="form-group">
<label for="preset-delay">
Delay (ms) *
<span id="delay-value-display" style="margin-left: 12px; color: #667eea; font-weight: 600;">100</span>
</label>
<input type="range" id="preset-delay" min="10" max="1000" value="100" step="10" required>
<small>Animation speed (10-1000 milliseconds)</small>
</div>
<div class="form-group">
<label for="step-offset">Step Offset</label>
<input type="number" id="step-offset" value="0" min="-1000" max="1000">
<small>Step offset for group synchronization. Applied per device when preset is used in a group.</small>
</div>
<div class="form-group">
<label for="step-increment">Step Increment</label>
<input type="number" id="step-increment" value="1" min="1" max="255">
<small>Amount step counter increments per cycle. Controls pattern advancement speed.</small>
</div>
<div class="form-group">
<label>Pattern Parameters (N1-N8)</label>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<small style="margin: 0;">Pattern-specific parameters (0-255, varies by pattern)</small>
<button type="button" class="btn btn-secondary" onclick="setAllNValues(0)" style="padding: 6px 12px; font-size: 0.75rem;">Reset All to 0</button>
</div>
<div class="params-grid">
<div class="param-input">
<label>N1</label>
<input type="number" id="n1" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N2</label>
<input type="number" id="n2" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N3</label>
<input type="number" id="n3" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N4</label>
<input type="number" id="n4" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N5</label>
<input type="number" id="n5" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N6</label>
<input type="number" id="n6" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N7</label>
<input type="number" id="n7" min="0" max="255" value="0" class="n-value-input">
</div>
<div class="param-input">
<label>N8</label>
<input type="number" id="n8" min="0" max="255" value="0" class="n-value-input">
</div>
</div>
<div style="margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap;">
<button type="button" class="btn btn-secondary" onclick="setAllNValues(0)" style="padding: 8px 16px; font-size: 0.875rem;">Set All to 0</button>
<button type="button" class="btn btn-secondary" onclick="copyNValuesFromCurrent()" style="padding: 8px 16px; font-size: 0.875rem;">Copy from Current Settings</button>
<button type="button" class="btn btn-secondary" onclick="showNValueHelp()" style="padding: 8px 16px; font-size: 0.875rem;"> Parameter Help</button>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Save Preset</button>
</div>
</form>
</div>
</div>
<script>
let currentView = 'grid';
let editingPreset = null;
// Delay slider update
document.getElementById('preset-delay').addEventListener('input', function(e) {
document.getElementById('delay-value-display').textContent = e.target.value;
});
// Search functionality
document.getElementById('search-input').addEventListener('input', function(e) {
filterPresets();
});
// Filter functionality
document.getElementById('pattern-filter').addEventListener('change', function(e) {
filterPresets();
});
// Sort functionality
document.getElementById('sort-select').addEventListener('change', function(e) {
sortPresets(e.target.value);
});
function filterPresets() {
const search = document.getElementById('search-input').value.toLowerCase();
const patternFilter = document.getElementById('pattern-filter').value;
const cards = document.querySelectorAll('.preset-card');
cards.forEach(card => {
const name = card.dataset.name.toLowerCase();
const pattern = card.dataset.pattern;
const matchesSearch = name.includes(search);
const matchesPattern = !patternFilter || pattern === patternFilter;
if (matchesSearch && matchesPattern) {
card.style.display = '';
} else {
card.style.display = 'none';
}
});
}
function sortPresets(sortBy) {
const container = document.getElementById('presets-container');
const cards = Array.from(container.querySelectorAll('.preset-card'));
cards.sort((a, b) => {
if (sortBy === 'name') {
return a.dataset.name.localeCompare(b.dataset.name);
} else if (sortBy === 'recent') {
// In real implementation, would use actual usage data
return 0;
} else if (sortBy === 'created') {
// In real implementation, would use creation timestamps
return 0;
}
return 0;
});
cards.forEach(card => container.appendChild(card));
}
function setView(view) {
currentView = view;
const container = document.getElementById('presets-container');
const gridBtn = document.getElementById('view-grid');
const listBtn = document.getElementById('view-list');
if (view === 'grid') {
container.style.gridTemplateColumns = 'repeat(auto-fill, minmax(300px, 1fr))';
gridBtn.classList.add('active');
listBtn.classList.remove('active');
} else {
container.style.gridTemplateColumns = '1fr';
gridBtn.classList.remove('active');
listBtn.classList.add('active');
}
}
function showCreateModal() {
editingPreset = null;
document.getElementById('modal-title').textContent = 'Create Preset';
document.getElementById('preset-form').reset();
document.getElementById('preset-delay').value = 100;
document.getElementById('delay-value-display').textContent = '100';
// Reset to 2 colors
initializeColorPickers();
document.getElementById('preset-modal').classList.add('active');
}
// Initialize color pickers function (defined before showCreateModal)
const presetColorPickers = [];
function initializeColorPickers() {
const colorInputs = document.getElementById('color-inputs');
colorInputs.innerHTML = '';
presetColorPickers.length = 0;
addColorPicker('#FF0000');
addColorPicker('#0000FF');
}
function addColorPicker(color = '#00FF00') {
const colorInputs = document.getElementById('color-inputs');
const wrapper = document.createElement('div');
wrapper.className = 'color-input-wrapper';
colorInputs.appendChild(wrapper);
const picker = new ColorPicker(wrapper, {
initialColor: color,
onColorChange: (newColor) => {
console.log('Preset color changed:', newColor);
}
});
presetColorPickers.push(picker);
return picker;
}
function editPreset(name) {
editingPreset = name;
document.getElementById('modal-title').textContent = 'Edit Preset';
// In real implementation, would load preset data
document.getElementById('preset-name').value = name;
document.getElementById('preset-modal').classList.add('active');
}
function closeModal() {
document.getElementById('preset-modal').classList.remove('active');
editingPreset = null;
}
function applyPreset(name) {
alert(`Applying preset: ${name}\n(In real implementation, this would send preset configuration to device(s))`);
}
function deletePreset(name) {
if (confirm(`Delete preset "${name}"?`)) {
alert(`Preset "${name}" deleted\n(In real implementation, this would remove the preset from storage)`);
}
}
function syncPresets() {
if (confirm('Sync all presets to all devices?\nThis will send all presets from master to all devices via ESPNow.')) {
alert('Syncing presets to all devices...\n(In real implementation, this would send all presets via ESPNow to all devices)');
}
}
function setAllNValues(value) {
for (let i = 1; i <= 8; i++) {
document.getElementById(`n${i}`).value = value;
}
}
function copyNValuesFromCurrent() {
// In real implementation, this would copy from current device settings
alert('Copying N values from current device settings...\n(In real implementation, this would load current N1-N8 values from the active device)');
// Example: would set values like this:
// document.getElementById('n1').value = currentSettings.n1;
// ... etc
}
function showNValueHelp() {
const helpText = `
Pattern Parameter Guide:
Rainbow:
N1: Step increment (1-255, default: 1)
Pulse:
N1: Attack time in ms (0-255)
N2: Hold time in ms (0-255)
N3: Decay time in ms (0-255)
Chase:
N1: LEDs of color 0 (1-255)
N2: LEDs of color 1 (1-255)
N3: Step movement on odd steps (can be negative)
N4: Step movement on even steps (can be negative)
Circle:
N1: Head moves per second (1-255)
N2: Max length in LEDs (1-255)
N3: Tail moves per second (1-255)
N4: Min length in LEDs (0-255)
Other patterns:
N1-N8: Reserved for future pattern enhancements
All values range from 0-255 unless otherwise specified.
`;
alert(helpText);
}
// Form submission
document.getElementById('preset-form').addEventListener('submit', function(e) {
e.preventDefault();
const name = document.getElementById('preset-name').value;
const action = editingPreset ? 'updated' : 'created';
alert(`Preset "${name}" ${action}!\n(In real implementation, this would save the preset to storage)`);
closeModal();
});
// Close modal on outside click
document.getElementById('preset-modal').addEventListener('click', function(e) {
if (e.target === this) {
closeModal();
}
});
</script>
<script src="color-picker.js"></script>
</body>
</html>

491
docs/mockups/settings.html Normal file
View File

@@ -0,0 +1,491 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LED Driver - Settings</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
}
.header {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.header h1 {
color: #667eea;
margin-bottom: 8px;
}
.header p {
color: #666;
}
.card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
margin-bottom: 24px;
}
.card h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.5rem;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 12px;
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #333;
}
.form-group input[type="text"],
.form-group input[type="number"],
.form-group input[type="password"],
.form-group select {
width: 100%;
padding: 12px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: #667eea;
}
.form-group input[type="range"] {
width: 100%;
height: 8px;
border-radius: 4px;
background: #e0e0e0;
outline: none;
-webkit-appearance: none;
}
.form-group input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
}
.form-group input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
background: #667eea;
cursor: pointer;
border: none;
}
.value-display {
display: inline-block;
margin-left: 12px;
font-weight: 600;
color: #667eea;
min-width: 60px;
}
.form-group small {
display: block;
margin-top: 4px;
color: #666;
font-size: 0.875rem;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 12px;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.color-order {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.color-order-option {
flex: 1;
min-width: 120px;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.color-order-option:hover {
border-color: #667eea;
}
.color-order-option.selected {
border-color: #667eea;
background: #667eea;
color: white;
}
.color-order-option .color-boxes {
display: flex;
justify-content: center;
gap: 4px;
margin-top: 8px;
}
.color-box {
width: 30px;
height: 30px;
border-radius: 4px;
}
.color-box.r { background: #ff0000; }
.color-box.g { background: #00ff00; }
.color-box.b { background: #0000ff; }
.actions {
display: flex;
gap: 12px;
margin-top: 32px;
padding-top: 24px;
border-top: 2px solid #e0e0e0;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 8px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover {
background: #5568d3;
}
.btn-secondary {
background: #e0e0e0;
color: #333;
}
.btn-secondary:hover {
background: #d0d0d0;
}
.btn-danger {
background: #f44336;
color: white;
}
.btn-danger:hover {
background: #d32f2f;
}
.btn-full {
flex: 1;
}
.section-divider {
height: 1px;
background: #e0e0e0;
margin: 24px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Device Settings</h1>
<p>Configure your LED driver device settings</p>
</div>
<!-- Basic Settings -->
<div class="card">
<h2>Basic Settings</h2>
<div class="form-group">
<label>Device Name</label>
<input type="text" id="device-name" value="led-device1" placeholder="led-device1">
<small>Unique identifier for this device</small>
</div>
<div class="form-group">
<label>LED Pin</label>
<input type="number" id="led-pin" value="10" min="0" max="40">
<small>GPIO pin number connected to LED data line</small>
</div>
<div class="form-group">
<label>Number of LEDs</label>
<input type="number" id="num-leds" value="50" min="1" max="1000">
<small>Total number of LEDs in your strip</small>
</div>
<div class="form-group">
<label>Color Order</label>
<div class="color-order">
<div class="color-order-option selected" data-order="rgb">
RGB
<div class="color-boxes">
<div class="color-box r"></div>
<div class="color-box g"></div>
<div class="color-box b"></div>
</div>
</div>
<div class="color-order-option" data-order="rbg">
RBG
<div class="color-boxes">
<div class="color-box r"></div>
<div class="color-box b"></div>
<div class="color-box g"></div>
</div>
</div>
<div class="color-order-option" data-order="grb">
GRB
<div class="color-boxes">
<div class="color-box g"></div>
<div class="color-box r"></div>
<div class="color-box b"></div>
</div>
</div>
<div class="color-order-option" data-order="gbr">
GBR
<div class="color-boxes">
<div class="color-box g"></div>
<div class="color-box b"></div>
<div class="color-box r"></div>
</div>
</div>
<div class="color-order-option" data-order="brg">
BRG
<div class="color-boxes">
<div class="color-box b"></div>
<div class="color-box r"></div>
<div class="color-box g"></div>
</div>
</div>
<div class="color-order-option" data-order="bgr">
BGR
<div class="color-boxes">
<div class="color-box b"></div>
<div class="color-box g"></div>
<div class="color-box r"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Pattern Settings -->
<div class="card">
<h2>Pattern Settings</h2>
<div class="form-group">
<label>Pattern</label>
<select id="pattern">
<option value="on">On</option>
<option value="off">Off</option>
<option value="blink">Blink</option>
<option value="chase">Chase</option>
<option value="circle">Circle</option>
<option value="pulse">Pulse</option>
<option value="rainbow">Rainbow</option>
<option value="transition">Transition</option>
</select>
</div>
<div class="form-group">
<label>
Brightness
<span class="value-display" id="brightness-value">100</span>%
</label>
<input type="range" id="brightness" min="0" max="100" value="100">
</div>
<div class="form-group">
<label>
Delay
<span class="value-display" id="delay-value">100</span>ms
</label>
<input type="range" id="delay" min="10" max="1000" value="100" step="10">
</div>
</div>
<!-- Advanced Settings -->
<div class="card">
<h2>Advanced Settings</h2>
<div class="form-group">
<label>Step Counter</label>
<input type="text" id="step-counter" value="0" readonly style="background: #f5f5f5; cursor: not-allowed;">
<small>Current step position in pattern (read-only)</small>
</div>
<div class="form-group">
<label for="step-increment">
Step Increment
</label>
<input type="number" id="step-increment" value="1" min="1" max="255">
<small>Amount step counter increments per cycle. Controls pattern advancement speed.</small>
</div>
<div class="form-group">
<label>Pattern Parameters</label>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px;">
<div>
<label style="font-size: 0.875rem;">N1</label>
<input type="number" id="n1" value="0" min="0" max="255">
</div>
<div>
<label style="font-size: 0.875rem;">N2</label>
<input type="number" id="n2" value="0" min="0" max="255">
</div>
<div>
<label style="font-size: 0.875rem;">N3</label>
<input type="number" id="n3" value="0" min="0" max="255">
</div>
<div>
<label style="font-size: 0.875rem;">N4</label>
<input type="number" id="n4" value="0" min="0" max="255">
</div>
<div>
<label style="font-size: 0.875rem;">N5</label>
<input type="number" id="n5" value="0" min="0" max="255">
</div>
<div>
<label style="font-size: 0.875rem;">N6</label>
<input type="number" id="n6" value="0" min="0" max="255">
</div>
</div>
<small>Pattern-specific parameters (varies by pattern)</small>
</div>
<div class="form-group">
<label>Device ID</label>
<input type="number" id="device-id" value="1" min="0">
<small>Unique numeric identifier</small>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="debug" checked>
<label for="debug" style="margin: 0;">Debug Mode</label>
</div>
<small>Enable debug logging</small>
</div>
</div>
<!-- Network Settings -->
<div class="card">
<h2>Network Settings</h2>
<div class="form-group">
<label>Access Point Name</label>
<input type="text" id="ap-name" value="led-AA:BB:CC:DD:EE:01" placeholder="led-device">
<small>WiFi access point name for device configuration</small>
</div>
<div class="form-group">
<label>Access Point Password</label>
<input type="password" id="ap-password" placeholder="Leave empty for open network">
<small>Password for the access point (optional)</small>
</div>
<div class="form-group">
<div class="checkbox-group">
<input type="checkbox" id="ap-enabled" checked>
<label for="ap-enabled" style="margin: 0;">Enable Access Point</label>
</div>
<small>Allow device to create its own WiFi network</small>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="actions">
<button class="btn btn-secondary btn-full" onclick="resetSettings()">Reset to Defaults</button>
<button class="btn btn-primary btn-full" onclick="saveSettings()">Save Settings</button>
</div>
</div>
</div>
<script>
// Brightness slider
document.getElementById('brightness').addEventListener('input', function(e) {
document.getElementById('brightness-value').textContent = e.target.value;
});
// Delay slider
document.getElementById('delay').addEventListener('input', function(e) {
document.getElementById('delay-value').textContent = e.target.value;
});
// Color order selection
document.querySelectorAll('.color-order-option').forEach(option => {
option.addEventListener('click', function() {
document.querySelectorAll('.color-order-option').forEach(o => o.classList.remove('selected'));
this.classList.add('selected');
});
});
function saveSettings() {
alert('Settings saved! (This is a mockup)');
}
function resetSettings() {
if (confirm('Reset all settings to defaults?')) {
alert('Settings reset! (This is a mockup)');
}
}
</script>
</body>
</html>