Add dashboard crosslinks and card drag-and-drop reordering

Dashboard cards (targets, auto-start, profiles) are now clickable,
navigating to the full entity card on the appropriate tab. Card
sections support drag-and-drop reordering via grip handles with
localStorage persistence. Fix crosslink navigation scoping to avoid
matching dashboard cards, and fix highlight race on rapid clicks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 00:40:37 +03:00
parent 88abd31c1c
commit 9194b978e0
8 changed files with 364 additions and 38 deletions

View File

@@ -447,7 +447,10 @@ export async function loadDashboard(forceFullRender = false) {
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
const subtitle = subtitleParts.length ? `<div class="dashboard-target-subtitle">${escapeHtml(subtitleParts.join(' · '))}</div>` : '';
return `<div class="dashboard-target dashboard-autostart" data-target-id="${target.id}">
const asNavSub = isLed ? 'led' : 'key_colors';
const asNavSec = isLed ? 'led-targets' : 'kc-targets';
const asNavAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-target-id="${target.id}" onclick="if(!event.target.closest('button')){navigateToCard('targets','${asNavSub}','${asNavSec}','${asNavAttr}','${target.id}')}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
<div>
@@ -551,6 +554,10 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
const isLed = target.target_type === 'led' || target.target_type === 'wled';
const icon = ICON_TARGET;
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
const navSubTab = isLed ? 'led' : 'key_colors';
const navSection = isLed ? 'led-targets' : 'kc-targets';
const navAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
const navOnclick = `if(!event.target.closest('button')){navigateToCard('targets','${navSubTab}','${navSection}','${navAttr}','${target.id}')}`;
let subtitleParts = [typeLabel];
if (isLed) {
@@ -590,7 +597,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
healthDot = `<span class="health-dot ${cls}"></span>`;
}
return `<div class="dashboard-target" data-target-id="${target.id}">
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${icon}</span>
<div>
@@ -620,7 +627,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
</div>
</div>`;
} else {
return `<div class="dashboard-target">
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${icon}</span>
<div>
@@ -665,7 +672,7 @@ function renderDashboardProfile(profile) {
const activeCount = (profile.active_target_ids || []).length;
const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`;
return `<div class="dashboard-target dashboard-profile" data-profile-id="${profile.id}">
return `<div class="dashboard-target dashboard-profile dashboard-card-link" data-profile-id="${profile.id}" onclick="if(!event.target.closest('button')){navigateToCard('profiles',null,'profiles','data-profile-id','${profile.id}')}">
<div class="dashboard-target-info">
<span class="dashboard-target-icon">${ICON_PROFILE}</span>
<div>