Add semi-transparent blurred tab icon as background watermark

Large SVG icon on the right side of the viewport reflects the active tab,
crossfades on tab switch. Also removes overflow:hidden from cards to fix
color picker clipping.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 11:43:59 +03:00
parent 4245e81a35
commit e97ef3afa6
4 changed files with 82 additions and 0 deletions

View File

@@ -102,6 +102,35 @@ body.modal-open {
background: transparent; background: transparent;
} }
/* ── Tab indicator (background watermark) ── */
#tab-indicator {
position: fixed;
right: -10vw;
top: 50%;
transform: translateY(-50%);
width: 55vw;
height: 55vw;
max-width: 600px;
max-height: 600px;
pointer-events: none;
z-index: -1;
opacity: 0;
filter: blur(6px);
color: var(--primary-color);
transition: opacity 0.5s ease;
}
#tab-indicator svg {
width: 100%;
height: 100%;
opacity: 0.04;
}
#tab-indicator.tab-indicator-visible {
opacity: 1;
}
[data-theme="light"] #tab-indicator svg {
opacity: 0.035;
}
.container { .container {
padding: 20px; padding: 20px;
} }

View File

@@ -13,6 +13,7 @@ import { t, initLocale, changeLocale } from './core/i18n.js';
// Layer 1.5: visual effects // Layer 1.5: visual effects
import { initCardGlare } from './core/card-glare.js'; import { initCardGlare } from './core/card-glare.js';
import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.js'; import { initBgAnim, updateBgAnimAccent, updateBgAnimTheme } from './core/bg-anim.js';
import { initTabIndicator, updateTabIndicator } from './core/tab-indicator.js';
// Layer 2: ui // Layer 2: ui
import { import {
@@ -173,6 +174,7 @@ Object.assign(window, {
// visual effects (called from inline <script>) // visual effects (called from inline <script>)
_updateBgAnimAccent: updateBgAnimAccent, _updateBgAnimAccent: updateBgAnimAccent,
_updateBgAnimTheme: updateBgAnimTheme, _updateBgAnimTheme: updateBgAnimTheme,
_updateTabIndicator: updateTabIndicator,
// core / ui // core / ui
toggleHint, toggleHint,
@@ -539,6 +541,7 @@ document.addEventListener('DOMContentLoaded', async () => {
// Initialize visual effects // Initialize visual effects
initCardGlare(); initCardGlare();
initBgAnim(); initBgAnim();
initTabIndicator();
updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light'); updateBgAnimTheme(document.documentElement.getAttribute('data-theme') !== 'light');
const accent = localStorage.getItem('accentColor') || '#4CAF50'; const accent = localStorage.getItem('accentColor') || '#4CAF50';
updateBgAnimAccent(accent); updateBgAnimAccent(accent);

View File

@@ -0,0 +1,47 @@
/**
* Tab indicator — large semi-transparent blurred icon on the right side
* of the viewport, reflecting the currently active tab.
*/
const TAB_SVGS = {
dashboard: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><rect width="7" height="9" x="3" y="3" rx="1"/><rect width="7" height="5" x="14" y="3" rx="1"/><rect width="7" height="9" x="14" y="12" rx="1"/><rect width="7" height="5" x="3" y="16" rx="1"/></svg>`,
automations: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg>`,
targets: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg>`,
streams: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2-5 5-5-5"/><rect width="20" height="15" x="2" y="7" rx="2"/></svg>`,
};
let _el = null;
let _currentTab = null;
function _ensureEl() {
if (_el) return _el;
_el = document.createElement('div');
_el.id = 'tab-indicator';
_el.setAttribute('aria-hidden', 'true');
document.body.appendChild(_el);
return _el;
}
export function updateTabIndicator(tabName) {
if (tabName === _currentTab) return;
_currentTab = tabName;
const svg = TAB_SVGS[tabName];
if (!svg) return;
const el = _ensureEl();
// Trigger crossfade: set opacity 0, swap content, fade in
el.classList.remove('tab-indicator-visible');
setTimeout(() => {
el.innerHTML = svg;
el.classList.add('tab-indicator-visible');
}, 200);
}
export function initTabIndicator() {
_ensureEl();
// Set initial tab from current active button
const active = document.querySelector('.tab-btn.active');
if (active) {
updateTabIndicator(active.dataset.tab);
}
}

View File

@@ -37,6 +37,9 @@ export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`)); document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
localStorage.setItem('activeTab', name); localStorage.setItem('activeTab', name);
// Update background tab indicator
if (typeof window._updateTabIndicator === 'function') window._updateTabIndicator(name);
// Restore scroll position for this tab // Restore scroll position for this tab
requestAnimationFrame(() => window.scrollTo(0, _tabScrollPositions[name] || 0)); requestAnimationFrame(() => window.scrollTo(0, _tabScrollPositions[name] || 0));