Files
wled-screen-controller-mixed/server/src/wled_controller/templates/modals/css-editor.html
alexei.dolgolyov aa1e4a6afc Add sync clock entity for synchronized animation timing
Introduces Synchronization Clocks — shared, controllable time bases
that CSS sources can optionally reference for synchronized animation.

Backend:
- New SyncClock dataclass, JSON store, Pydantic schemas, REST API
- Runtime clock with thread-safe pause/resume/reset and speed control
- Ref-counted runtime pool with eager creation for API control
- clock_id field on all ColorStripSource types
- Stream integration: clock time/speed replaces source-local values
- Paused clock skips rendering (saves CPU + stops frame pushes)
- Included in backup/restore via STORE_MAP

Frontend:
- Sync Clocks tab in Streams section with cards and controls
- Clock dropdown in CSS editor (hidden speed slider when clock set)
- Clock crosslink badge on CSS source cards (replaces speed badge)
- Targets tab uses DataCache for picture/audio sources and sync clocks

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:46:55 +03:00

532 lines
42 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Color Strip Source Editor Modal -->
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="css-editor-title"><svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></svg> <span data-i18n="color_strip.add">Add Color Strip Source</span></h2>
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="css-editor-form">
<input type="hidden" id="css-editor-id">
<div class="form-group">
<label for="css-editor-name" data-i18n="color_strip.name">Name:</label>
<input type="text" id="css-editor-name" data-i18n-placeholder="color_strip.name.placeholder" placeholder="Wall Strip" required>
</div>
<div id="css-editor-type-group" class="form-group">
<div class="label-row">
<label for="css-editor-type" data-i18n="color_strip.type">Type:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.type.hint">Picture Source derives colors from a screen capture. Static Color fills all LEDs with one constant color. Gradient distributes a color gradient across all LEDs.</small>
<select id="css-editor-type" onchange="onCSSTypeChange()">
<option value="picture" data-i18n="color_strip.type.picture">Picture Source</option>
<option value="static" data-i18n="color_strip.type.static">Static Color</option>
<option value="gradient" data-i18n="color_strip.type.gradient">Gradient</option>
<option value="color_cycle" data-i18n="color_strip.type.color_cycle">Color Cycle</option>
<option value="effect" data-i18n="color_strip.type.effect">Procedural Effect</option>
<option value="composite" data-i18n="color_strip.type.composite">Composite</option>
<option value="mapped" data-i18n="color_strip.type.mapped">Mapped</option>
<option value="audio" data-i18n="color_strip.type.audio">Audio Reactive</option>
<option value="api_input" data-i18n="color_strip.type.api_input">API Input</option>
</select>
</div>
<!-- Picture-source-specific fields -->
<div id="css-editor-picture-section">
<div class="form-group">
<div class="label-row">
<label for="css-editor-picture-source" data-i18n="color_strip.picture_source">Picture Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.picture_source.hint">Which screen capture source to use as input for LED color calculation</small>
<select id="css-editor-picture-source"></select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-interpolation" data-i18n="color_strip.interpolation">Color Mode:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.interpolation.hint">How to calculate LED color from sampled border pixels</small>
<select id="css-editor-interpolation">
<option value="average" data-i18n="color_strip.interpolation.average">Average</option>
<option value="median" data-i18n="color_strip.interpolation.median">Median</option>
<option value="dominant" data-i18n="color_strip.interpolation.dominant">Dominant</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-smoothing">
<span data-i18n="color_strip.smoothing">Smoothing:</span>
<span id="css-editor-smoothing-value">0.30</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.smoothing.hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-frame-interpolation" data-i18n="color_strip.frame_interpolation">Frame Interpolation:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.frame_interpolation.hint">Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-frame-interpolation">
<span class="settings-toggle-slider"></span>
</label>
</div>
<details class="form-collapse">
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-brightness">
<span data-i18n="color_strip.brightness">Brightness:</span>
<span id="css-editor-brightness-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-saturation">
<span data-i18n="color_strip.saturation">Saturation:</span>
<span id="css-editor-saturation-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-gamma">
<span data-i18n="color_strip.gamma">Gamma:</span>
<span id="css-editor-gamma-value">1.00</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gamma.hint">Gamma correction (1=none, &lt;1=brighter midtones, &gt;1=darker midtones)</small>
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
</div>
</details>
</div>
<!-- Static-color-specific fields -->
<div id="css-editor-static-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-color" data-i18n="color_strip.static_color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.static_color.hint">The solid color that will be sent to all LEDs on the strip.</small>
<input type="color" id="css-editor-color" value="#ffffff">
</div>
</div>
<!-- Color-cycle-specific fields -->
<div id="css-editor-color-cycle-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.color_cycle.colors">Colors:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
<div id="color-cycle-colors-list"></div>
<button type="button" class="btn btn-secondary" onclick="colorCycleAddColor()" data-i18n="color_strip.color_cycle.add_color">+ Add Color</button>
</div>
<div id="css-editor-cycle-speed-group" class="form-group">
<div class="label-row">
<label for="css-editor-cycle-speed">
<span data-i18n="color_strip.color_cycle.speed">Speed:</span>
<span id="css-editor-cycle-speed-val">1.0</span>×
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.speed.hint">Cycle speed multiplier. 1.0 ≈ one full cycle every 20 seconds.</small>
<input type="range" id="css-editor-cycle-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-cycle-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
</div>
<!-- Gradient-specific fields -->
<div id="css-editor-gradient-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.preset">Preset:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small>
<select id="css-editor-gradient-preset" onchange="applyGradientPreset(this.value)">
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
<option value="ocean" data-i18n="color_strip.gradient.preset.ocean">Ocean</option>
<option value="forest" data-i18n="color_strip.gradient.preset.forest">Forest</option>
<option value="fire" data-i18n="color_strip.gradient.preset.fire">Fire</option>
<option value="lava" data-i18n="color_strip.gradient.preset.lava">Lava</option>
<option value="aurora" data-i18n="color_strip.gradient.preset.aurora">Aurora</option>
<option value="ice" data-i18n="color_strip.gradient.preset.ice">Ice</option>
<option value="warm" data-i18n="color_strip.gradient.preset.warm">Warm</option>
<option value="cool" data-i18n="color_strip.gradient.preset.cool">Cool</option>
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.preview">Gradient:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.preview.hint">Visual preview. Click the marker track below to add a stop. Drag markers to reposition.</small>
<div class="gradient-editor">
<canvas id="gradient-canvas" height="44"></canvas>
<div id="gradient-markers-track" class="gradient-markers-track"></div>
</div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.gradient.stops">Color Stops:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.stops.hint">Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.</small>
<div id="gradient-stops-list"></div>
</div>
</div>
<!-- Procedural effect fields -->
<div id="css-editor-effect-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-effect-type" data-i18n="color_strip.effect.type">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.type.hint">The procedural effect algorithm to use.</small>
<select id="css-editor-effect-type" onchange="onEffectTypeChange()">
<option value="fire" data-i18n="color_strip.effect.fire">Fire</option>
<option value="meteor" data-i18n="color_strip.effect.meteor">Meteor</option>
<option value="plasma" data-i18n="color_strip.effect.plasma">Plasma</option>
<option value="noise" data-i18n="color_strip.effect.noise">Perlin Noise</option>
<option value="aurora" data-i18n="color_strip.effect.aurora">Aurora</option>
</select>
<small id="css-editor-effect-type-desc" class="field-desc"></small>
<div id="css-editor-effect-preview" class="effect-palette-preview"></div>
</div>
<div id="css-editor-effect-speed-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-speed">
<span data-i18n="color_strip.effect.speed">Speed:</span>
<span id="css-editor-effect-speed-val">1.0</span>x
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.speed.hint">How fast the effect animates. 1.0 = default speed.</small>
<input type="range" id="css-editor-effect-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-palette" data-i18n="color_strip.effect.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.palette.hint">Color palette used by the effect.</small>
<select id="css-editor-effect-palette" onchange="updateEffectPreview()">
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
</select>
</div>
<div id="css-editor-effect-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-color" data-i18n="color_strip.effect.color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.color.hint">Head color for the meteor effect.</small>
<input type="color" id="css-editor-effect-color" value="#ff5000" oninput="updateEffectPreview()">
</div>
<div id="css-editor-effect-intensity-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-intensity">
<span data-i18n="color_strip.effect.intensity">Intensity:</span>
<span id="css-editor-effect-intensity-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.intensity.hint">Effect-specific intensity.</small>
<input type="range" id="css-editor-effect-intensity" min="0.1" max="2.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-scale-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-scale">
<span data-i18n="color_strip.effect.scale">Scale:</span>
<span id="css-editor-effect-scale-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.scale.hint">Spatial zoom level.</small>
<input type="range" id="css-editor-effect-scale" min="0.5" max="5.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div id="css-editor-effect-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-mirror" data-i18n="color_strip.effect.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.mirror.hint">Bounce back and forth instead of wrapping around.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-effect-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- Composite-specific fields -->
<div id="css-editor-composite-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.composite.layers">Layers:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.composite.layers.hint">Stack multiple color strip sources. First layer is the bottom, last is the top.</small>
<div id="composite-layers-list"></div>
<button type="button" class="btn btn-secondary" onclick="compositeAddLayer()" data-i18n="color_strip.composite.add_layer">+ Add Layer</button>
</div>
</div>
<!-- Mapped-specific fields -->
<div id="css-editor-mapped-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.mapped.zones">Zones:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.mapped.zones.hint">Each zone maps a color strip source to a specific LED range. Zones are placed side-by-side.</small>
<div id="mapped-zones-list"></div>
<button type="button" class="btn btn-secondary" onclick="mappedAddZone()" data-i18n="color_strip.mapped.add_zone">+ Add Zone</button>
</div>
</div>
<!-- Audio-reactive fields -->
<div id="css-editor-audio-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-viz" data-i18n="color_strip.audio.visualization">Visualization:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.visualization.hint">How audio data is rendered to LEDs.</small>
<select id="css-editor-audio-viz" onchange="onAudioVizChange()">
<option value="spectrum" data-i18n="color_strip.audio.viz.spectrum">Spectrum Analyzer</option>
<option value="beat_pulse" data-i18n="color_strip.audio.viz.beat_pulse">Beat Pulse</option>
<option value="vu_meter" data-i18n="color_strip.audio.viz.vu_meter">VU Meter</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-source" data-i18n="color_strip.audio.source">Audio Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.source.hint">Mono audio source that provides audio data for this visualization. Create and manage audio sources in the Sources tab.</small>
<select id="css-editor-audio-source">
<!-- populated dynamically from /api/v1/audio-sources?source_type=mono -->
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-sensitivity">
<span data-i18n="color_strip.audio.sensitivity">Sensitivity:</span>
<span id="css-editor-audio-sensitivity-val">1.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.sensitivity.hint">Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.</small>
<input type="range" id="css-editor-audio-sensitivity" min="0.1" max="5.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-audio-sensitivity-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-smoothing">
<span data-i18n="color_strip.audio.smoothing">Smoothing:</span>
<span id="css-editor-audio-smoothing-val">0.30</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.smoothing.hint">Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.</small>
<input type="range" id="css-editor-audio-smoothing" min="0.0" max="1.0" step="0.05" value="0.3"
oninput="document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div id="css-editor-audio-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-audio-palette" data-i18n="color_strip.audio.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.palette.hint">Color palette used for spectrum bars or beat pulse coloring.</small>
<select id="css-editor-audio-palette">
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
</select>
</div>
<div id="css-editor-audio-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color" data-i18n="color_strip.audio.color">Base Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color.hint">Low-level color for VU meter bar.</small>
<input type="color" id="css-editor-audio-color" value="#00ff00">
</div>
<div id="css-editor-audio-color-peak-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color-peak" data-i18n="color_strip.audio.color_peak">Peak Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color_peak.hint">High-level color at the top of the VU meter bar.</small>
<input type="color" id="css-editor-audio-color-peak" value="#ff0000">
</div>
<div id="css-editor-audio-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-mirror" data-i18n="color_strip.audio.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.mirror.hint">Mirror spectrum from center outward: bass in the middle, treble at the edges.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-audio-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- API Input fields -->
<div id="css-editor-api-input-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-api-input-fallback-color" data-i18n="color_strip.api_input.fallback_color">Fallback Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.fallback_color.hint">Color to display when no data has been received within the timeout period.</small>
<input type="color" id="css-editor-api-input-fallback-color" value="#000000">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-api-input-timeout">
<span data-i18n="color_strip.api_input.timeout">Timeout (seconds):</span>
<span id="css-editor-api-input-timeout-val">5.0</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.timeout.hint">How long to wait for new color data before reverting to the fallback color.</small>
<input type="range" id="css-editor-api-input-timeout" min="0" max="60" step="0.5" value="5.0"
oninput="document.getElementById('css-editor-api-input-timeout-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div class="form-group" id="css-editor-api-input-endpoints-group">
<div class="label-row">
<label data-i18n="color_strip.api_input.endpoints">Push Endpoints:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.endpoints.hint">Use these URLs to push LED color data from your external application.</small>
<div id="css-editor-api-input-endpoints" class="template-config" style="font-family:monospace; font-size:0.85em; word-break:break-all;"></div>
</div>
</div>
<!-- Shared LED count field -->
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to auto-detect from calibration or device.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<!-- Animation — shown for static/gradient, hidden for picture -->
<div id="css-editor-animation-section" style="display:none">
<details class="form-collapse">
<summary><span data-i18n="color_strip.animation">Animation</span></summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-animation-type" data-i18n="color_strip.animation.type">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.type.hint">The animation effect to apply. Available effects depend on source type. Select None to disable animation.</small>
<select id="css-editor-animation-type" onchange="onAnimationTypeChange()">
<!-- populated by onCSSTypeChange() -->
</select>
<small id="css-editor-animation-type-desc" class="field-desc"></small>
</div>
<div id="css-editor-animation-speed-group" class="form-group">
<div class="label-row">
<label for="css-editor-animation-speed">
<span data-i18n="color_strip.animation.speed">Speed:</span>
<span id="css-editor-animation-speed-val">1.0</span>×
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.speed.hint">Animation speed multiplier. 1.0 ≈ one cycle per second for Breathing; higher values cycle faster.</small>
<input type="range" id="css-editor-animation-speed" min="0.1" max="10.0" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-animation-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
</div>
</details>
</div>
<!-- Sync Clock (shown for animated types: static, gradient, color_cycle, effect) -->
<div id="css-editor-clock-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-clock" data-i18n="color_strip.clock">Sync Clock:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.clock.hint">Optionally link to a sync clock to synchronize animation timing and speed with other sources</small>
<select id="css-editor-clock" onchange="onCSSClockChange()">
<option value="" data-i18n="common.none">None</option>
</select>
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeCSSEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveCSSEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>