Move Scenes into Automations tab, smaller Capture button, scene crosslinks

- Merge Scenes tab into Automations tab as a second CardSection below automations
- Make dashboard Capture button match Stop All sizing
- Dashboard scene cards navigate to automations tab on click (crosslink)
- Add scene steps to automations tutorial
- Fix tour.tgt.devices to say "LED controllers" instead of "WLED controllers"
- Update command palette and navigation for new scene location

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 18:16:21 +03:00
parent 21248e2dc9
commit 39b31aec34
11 changed files with 39 additions and 86 deletions

View File

@@ -83,7 +83,6 @@ import {
expandAllAutomationSections, collapseAllAutomationSections,
} from './features/automations.js';
import {
loadScenes, expandAllSceneSections, collapseAllSceneSections,
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
activateScenePreset, recaptureScenePreset, deleteScenePreset,
} from './features/scene-presets.js';
@@ -312,9 +311,6 @@ Object.assign(window, {
collapseAllAutomationSections,
// scene presets
loadScenes,
expandAllSceneSections,
collapseAllSceneSections,
openScenePresetCapture,
editScenePreset,
saveScenePreset,
@@ -438,9 +434,9 @@ document.addEventListener('keydown', (e) => {
return;
}
// Tab shortcuts: Ctrl+1..5 (skip when typing in inputs)
// Tab shortcuts: Ctrl+1..4 (skip when typing in inputs)
if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'scenes' };
const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams' };
const tab = tabMap[e.key];
if (tab) {
e.preventDefault();

View File

@@ -101,7 +101,7 @@ function _buildItems(results) {
_mapEntities(scenePresets, sp => items.push({
name: sp.name, detail: sp.description || '', group: 'scenes', icon: ICON_SCENE,
nav: ['scenes', null, 'scenes', 'data-scene-id', sp.id],
nav: ['automations', null, 'scenes', 'data-scene-id', sp.id],
}));
return items;

View File

@@ -90,7 +90,6 @@ function _triggerTabLoad(tab) {
else if (tab === 'automations' && typeof window.loadAutomations === 'function') window.loadAutomations();
else if (tab === 'streams' && typeof window.loadPictureSources === 'function') window.loadPictureSources();
else if (tab === 'targets' && typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
else if (tab === 'scenes' && typeof window.loadScenes === 'function') window.loadScenes();
}
function _showDimOverlay(duration) {

View File

@@ -10,6 +10,7 @@ import { Modal } from '../core/modal.js';
import { CardSection } from '../core/card-sections.js';
import { updateTabBadge } from './tabs.js';
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
import { csScenes, createSceneCard } from './scene-presets.js';
// ===== Scene presets cache (shared by both selectors) =====
let _scenesCache = [];
@@ -80,22 +81,25 @@ export async function loadAutomations() {
}
export function expandAllAutomationSections() {
CardSection.expandAll([csAutomations]);
CardSection.expandAll([csAutomations, csScenes]);
}
export function collapseAllAutomationSections() {
CardSection.collapseAll([csAutomations]);
CardSection.collapseAll([csAutomations, csScenes]);
}
function renderAutomations(automations, sceneMap) {
const container = document.getElementById('automations-content');
const items = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
container.innerHTML = toolbar + csAutomations.render(items);
csAutomations.bind();
const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
const sceneItems = csScenes.applySortOrder(_scenesCache.map(s => ({ key: s.id, html: createSceneCard(s) })));
// Localize data-i18n elements within the automations container only
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
container.innerHTML = toolbar + csAutomations.render(autoItems) + csScenes.render(sceneItems);
csAutomations.bind();
csScenes.bind();
// Localize data-i18n elements within the container
container.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});

View File

@@ -1,22 +1,19 @@
/**
* Scene Presets — capture, activate, edit, delete system state snapshots.
* Renders as a dedicated tab and also provides dashboard section rendering.
* Rendered as a CardSection inside the Automations tab, plus dashboard compact cards.
*/
import { apiKey } from '../core/state.js';
import { fetchWithAuth, escapeHtml } from '../core/api.js';
import { t } from '../core/i18n.js';
import { showToast, showConfirm } from '../core/ui.js';
import { Modal } from '../core/modal.js';
import { CardSection } from '../core/card-sections.js';
import { updateTabBadge } from './tabs.js';
import {
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_SETTINGS,
} from '../core/icons.js';
let _presetsCache = [];
let _editingId = null;
let _scenesLoading = false;
class ScenePresetEditorModal extends Modal {
constructor() { super('scene-preset-editor-modal'); }
@@ -30,59 +27,14 @@ class ScenePresetEditorModal extends Modal {
}
const scenePresetModal = new ScenePresetEditorModal();
const csScenes = new CardSection('scenes', {
export const csScenes = new CardSection('scenes', {
titleKey: 'scenes.title',
gridClass: 'devices-grid',
addCardOnclick: "openScenePresetCapture()",
keyAttr: 'data-scene-id',
});
// Re-render scenes when language changes (only if tab is active)
document.addEventListener('languageChanged', () => {
if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'scenes') loadScenes();
});
// ===== Tab rendering =====
export async function loadScenes() {
if (_scenesLoading) return;
_scenesLoading = true;
try {
const resp = await fetchWithAuth('/scene-presets');
if (!resp.ok) { _scenesLoading = false; return; }
const data = await resp.json();
_presetsCache = data.presets || [];
} catch {
_scenesLoading = false;
return;
}
const container = document.getElementById('scenes-content');
const items = csScenes.applySortOrder(_presetsCache.map(p => ({ key: p.id, html: _createSceneCard(p) })));
updateTabBadge('scenes', _presetsCache.length);
if (csScenes.isMounted()) {
csScenes.reconcile(items);
} else {
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllSceneSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllSceneSections()" title="${t('section.collapse_all')}">⊟</button></span></div>`;
container.innerHTML = toolbar + csScenes.render(items);
csScenes.bind();
}
_scenesLoading = false;
}
export function expandAllSceneSections() {
CardSection.expandAll([csScenes]);
}
export function collapseAllSceneSections() {
CardSection.collapseAll([csScenes]);
}
function _createSceneCard(preset) {
export function createSceneCard(preset) {
const targetCount = (preset.targets || []).length;
const deviceCount = (preset.devices || []).length;
const automationCount = (preset.automations || []).length;
@@ -133,7 +85,7 @@ export async function loadScenePresets() {
export function renderScenePresetsSection(presets) {
if (!presets || presets.length === 0) return '';
const captureBtn = `<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
const captureBtn = `<button class="btn btn-sm btn-primary dashboard-stop-all" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
const cards = presets.map(p => _renderDashboardPresetCard(p)).join('');
return { headerExtra: captureBtn, content: `<div class="dashboard-autostart-grid">${cards}</div>` };
@@ -151,8 +103,8 @@ function _renderDashboardPresetCard(preset) {
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
].filter(Boolean).join(' \u00b7 ');
return `<div class="dashboard-target dashboard-scene-preset" data-scene-id="${preset.id}" style="${borderStyle}">
<div class="dashboard-target-info" onclick="activateScenePreset('${preset.id}')">
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_SCENE}</span>
<div>
<div class="dashboard-target-name">${escapeHtml(preset.name)}</div>
@@ -327,9 +279,9 @@ export async function deleteScenePreset(presetId) {
// ===== Helpers =====
function _reloadScenesTab() {
// Reload the scenes tab if it's active
if ((localStorage.getItem('activeTab') || 'dashboard') === 'scenes') {
loadScenes();
// Reload automations tab (which includes scenes section)
if ((localStorage.getItem('activeTab') || 'dashboard') === 'automations') {
if (typeof window.loadAutomations === 'function') window.loadAutomations();
}
// Also refresh dashboard (scene presets section)
if (typeof window.loadDashboard === 'function') window.loadDashboard(true);

View File

@@ -60,8 +60,6 @@ export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
} else if (name === 'automations') {
if (typeof window.loadAutomations === 'function') window.loadAutomations();
} else if (name === 'scenes') {
if (typeof window.loadScenes === 'function') window.loadScenes();
}
}
}
@@ -82,8 +80,6 @@ export function initTabs() {
saved = localStorage.getItem('activeTab');
}
// Migrate legacy 'devices' tab to 'targets'
if (saved === 'devices') saved = 'targets';
if (!saved || !document.getElementById(`tab-${saved}`)) saved = 'dashboard';
switchTab(saved);
}

View File

@@ -62,7 +62,10 @@ const sourcesTourSteps = [
const automationsTutorialSteps = [
{ selector: '[data-card-section="automations"]', textKey: 'tour.auto.list', position: 'bottom' },
{ selector: '[data-cs-add="automations"]', textKey: 'tour.auto.add', position: 'bottom' },
{ selector: '.card[data-automation-id]', textKey: 'tour.auto.card', position: 'bottom' }
{ selector: '.card[data-automation-id]', textKey: 'tour.auto.card', position: 'bottom' },
{ selector: '[data-card-section="scenes"]', textKey: 'tour.auto.scenes_list', position: 'bottom' },
{ selector: '[data-cs-add="scenes"]', textKey: 'tour.auto.scenes_add', position: 'bottom' },
{ selector: '.card[data-scene-id]', textKey: 'tour.auto.scenes_card', position: 'bottom' },
];
const _fixedResolve = (step) => {