diff --git a/CLAUDE.md b/CLAUDE.md index 5138644..948809d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -149,6 +149,38 @@ Every editor modal **must** have a dirty check so closing with unsaved changes s The base class handles: `isDirty()` comparison, confirmation dialog, backdrop click, ESC key, focus trapping, and body scroll lock. +### Card appearance + +When creating or modifying entity cards (devices, targets, CSS sources, streams, audio/value sources, templates), **always reference existing cards** of the same or similar type for visual consistency. Cards should have: + +- Clone (📋) and Edit (✏️) icon buttons in `.template-card-actions` +- Delete (✕) button as `.card-remove-btn` +- Property badges in `.stream-card-props` with emoji icons + +### Modal footer buttons + +Use **icon-only** buttons (✓ / ✕) matching the device settings modal pattern, **not** text buttons: + +```html +
+``` + +### Slider value display + +For range sliders, display the current value **inside the label** (not in a separate wrapper). This keeps the value visible next to the property name: + +```html + +... + +``` + +Do **not** use a `range-with-value` wrapper div. + ## General Guidelines - Always test changes before marking as complete diff --git a/server/src/wled_controller/static/css/streams.css b/server/src/wled_controller/static/css/streams.css index 5c170b2..3b090fb 100644 --- a/server/src/wled_controller/static/css/streams.css +++ b/server/src/wled_controller/static/css/streams.css @@ -627,7 +627,6 @@ color: var(--text-secondary); margin: 0 0 12px 0; padding-bottom: 8px; - border-bottom: 1px solid var(--border-color); } .subtab-section-header.cs-header { diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 13b1e7a..31e9b1a 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -107,13 +107,13 @@ import { // Layer 5: audio sources import { showAudioSourceModal, closeAudioSourceModal, saveAudioSource, - editAudioSource, deleteAudioSource, onAudioSourceTypeChange, + editAudioSource, cloneAudioSource, deleteAudioSource, onAudioSourceTypeChange, } from './features/audio-sources.js'; // Layer 5: value sources import { showValueSourceModal, closeValueSourceModal, saveValueSource, - editValueSource, deleteValueSource, onValueSourceTypeChange, + editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange, addSchedulePoint, } from './features/value-sources.js'; @@ -328,6 +328,7 @@ Object.assign(window, { closeAudioSourceModal, saveAudioSource, editAudioSource, + cloneAudioSource, deleteAudioSource, onAudioSourceTypeChange, @@ -336,6 +337,7 @@ Object.assign(window, { closeValueSourceModal, saveValueSource, editValueSource, + cloneValueSource, deleteValueSource, onValueSourceTypeChange, addSchedulePoint, diff --git a/server/src/wled_controller/static/js/core/navigation.js b/server/src/wled_controller/static/js/core/navigation.js index 981e4ac..e7e1d0e 100644 --- a/server/src/wled_controller/static/js/core/navigation.js +++ b/server/src/wled_controller/static/js/core/navigation.js @@ -16,7 +16,11 @@ import { switchTab } from '../features/tabs.js'; export function navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue) { // Push current location to history so browser back returns here history.pushState(null, '', location.hash || '#'); - switchTab(tab); + + // Activate tab visually without triggering a data reload — + // the command palette already fetched fresh data, and a reload + // would re-render all cards, destroying the highlight. + switchTab(tab, { skipLoad: true }); requestAnimationFrame(() => { if (subTab) { @@ -41,17 +45,36 @@ export function navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue) { } } - // Wait for card to appear in DOM (tab data may load async) - _waitForCard(cardAttr, cardValue, 3000).then(card => { - if (!card) return; - card.scrollIntoView({ behavior: 'smooth', block: 'center' }); - card.classList.add('card-highlight'); - _showDimOverlay(2000); - setTimeout(() => card.classList.remove('card-highlight'), 2000); + // Check if card already exists (data previously loaded) + const existing = document.querySelector(`[${cardAttr}="${cardValue}"]`); + if (existing) { + _highlightCard(existing); + return; + } + + // Card not in DOM — trigger data load and wait for it to appear + _triggerTabLoad(tab); + _waitForCard(cardAttr, cardValue, 5000).then(card => { + if (card) _highlightCard(card); }); }); } +function _highlightCard(card) { + card.scrollIntoView({ behavior: 'smooth', block: 'center' }); + card.classList.add('card-highlight'); + _showDimOverlay(2000); + setTimeout(() => card.classList.remove('card-highlight'), 2000); +} + +/** Trigger the tab's data load function (used when card wasn't found in DOM). */ +function _triggerTabLoad(tab) { + if (tab === 'dashboard' && typeof window.loadDashboard === 'function') window.loadDashboard(); + else if (tab === 'profiles' && typeof window.loadProfiles === 'function') window.loadProfiles(); + else if (tab === 'streams' && typeof window.loadPictureSources === 'function') window.loadPictureSources(); + else if (tab === 'targets' && typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); +} + function _showDimOverlay(duration) { let overlay = document.getElementById('nav-dim-overlay'); if (!overlay) { diff --git a/server/src/wled_controller/static/js/features/audio-sources.js b/server/src/wled_controller/static/js/features/audio-sources.js index c13785a..1a9f198 100644 --- a/server/src/wled_controller/static/js/features/audio-sources.js +++ b/server/src/wled_controller/static/js/features/audio-sources.js @@ -149,6 +149,22 @@ export async function editAudioSource(sourceId) { } } +// ── Clone ───────────────────────────────────────────────────── + +export async function cloneAudioSource(sourceId) { + try { + const resp = await fetchWithAuth(`/audio-sources/${sourceId}`); + if (!resp.ok) throw new Error('fetch failed'); + const data = await resp.json(); + delete data.id; + data.name = data.name + ' (copy)'; + await showAudioSourceModal(data.source_type, data); + } catch (e) { + if (e.isAuth) return; + showToast(e.message, 'error'); + } +} + // ── Delete ──────────────────────────────────────────────────── export async function deleteAudioSource(sourceId) { diff --git a/server/src/wled_controller/static/js/features/streams.js b/server/src/wled_controller/static/js/features/streams.js index 43e2bf2..18ae935 100644 --- a/server/src/wled_controller/static/js/features/streams.js +++ b/server/src/wled_controller/static/js/features/streams.js @@ -748,6 +748,7 @@ function renderPictureSourcesList(streams) {