Fix mobile color picker popup clipping and locale update for tabs/sections

Color picker popover now uses fixed positioning on small screens to
escape the header toolbar overflow container. Section titles, sub-tab
labels, and filter placeholders use data-i18n attributes so they update
automatically on language change. Display picker title switches to
"Select a Device" for engine-owned display lists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 14:26:15 +03:00
parent ddfa7637d6
commit 6366b0b317
9 changed files with 61 additions and 32 deletions

View File

@@ -278,6 +278,7 @@ h2 {
}
.color-picker-popover.anchor-right { right: 0; }
.color-picker-popover.anchor-left { left: 0; }
.color-picker-popover.cp-fixed { z-index: 1000; }
@keyframes color-picker-pop-in {
from { opacity: 0; transform: translateY(-4px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }

View File

@@ -86,13 +86,13 @@ export class CardSection {
<div class="subtab-section${collapsedClass}" data-card-section="${this.sectionKey}">
<div class="subtab-section-header cs-header" data-cs-toggle="${this.sectionKey}">
<span class="cs-chevron"${chevronStyle}>&#9654;</span>
<span class="cs-title">${t(this.titleKey)}</span>
<span class="cs-title" data-i18n="${this.titleKey}">${t(this.titleKey)}</span>
<span class="cs-count">${count}</span>
${this.headerExtra ? `<span class="cs-header-extra">${this.headerExtra}</span>` : ''}
<div class="cs-filter-wrap">
<input type="text" class="cs-filter" data-cs-filter="${this.sectionKey}"
placeholder="${t('section.filter.placeholder')}" autocomplete="off">
<button type="button" class="cs-filter-reset" data-cs-filter-reset="${this.sectionKey}" title="${t('section.filter.reset')}">×</button>
data-i18n-placeholder="section.filter.placeholder" placeholder="${t('section.filter.placeholder')}" autocomplete="off">
<button type="button" class="cs-filter-reset" data-cs-filter-reset="${this.sectionKey}" data-i18n-title="section.filter.reset" title="${t('section.filter.reset')}">×</button>
</div>
</div>
<div class="cs-content ${this.gridClass}" data-cs-content="${this.sectionKey}"${contentDisplay}>

View File

@@ -73,29 +73,56 @@ window._cpToggle = function (id) {
// Close all other pickers first (and drop their card elevation)
document.querySelectorAll('.color-picker-popover').forEach(p => {
if (p.id !== `cp-pop-${id}`) {
p.style.display = 'none';
const card = p.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated');
_cpClosePopover(p);
}
});
const pop = document.getElementById(`cp-pop-${id}`);
if (!pop) return;
const show = pop.style.display === 'none';
pop.style.display = show ? '' : 'none';
if (!show) {
_cpClosePopover(pop);
return;
}
pop.style.display = '';
// Elevate the card so the popover isn't clipped by sibling cards
const card = pop.closest('.card, .template-card');
if (card) card.classList.toggle('cp-elevated', show);
if (show) {
// Mark active dot
const swatch = document.getElementById(`cp-swatch-${id}`);
const cur = swatch ? (_rgbToHex(swatch.style.backgroundColor) || swatch.style.background) : '';
pop.querySelectorAll('.color-picker-dot').forEach(d => {
const dHex = _rgbToHex(d.style.backgroundColor || d.style.background);
d.classList.toggle('active', dHex.toLowerCase() === cur.toLowerCase());
});
if (card) card.classList.toggle('cp-elevated', true);
// On small screens, if inside an overflow container (e.g. header toolbar),
// switch to fixed positioning so the popover isn't clipped.
const wrapper = pop.closest('.color-picker-wrapper');
if (wrapper && window.innerWidth <= 600) {
const rect = wrapper.getBoundingClientRect();
pop.style.position = 'fixed';
pop.style.top = (rect.bottom + 4) + 'px';
pop.style.right = Math.max(4, window.innerWidth - rect.right) + 'px';
pop.style.left = 'auto';
pop.classList.add('cp-fixed');
}
// Mark active dot
const swatch = document.getElementById(`cp-swatch-${id}`);
const cur = swatch ? (_rgbToHex(swatch.style.backgroundColor) || swatch.style.background) : '';
pop.querySelectorAll('.color-picker-dot').forEach(d => {
const dHex = _rgbToHex(d.style.backgroundColor || d.style.background);
d.classList.toggle('active', dHex.toLowerCase() === cur.toLowerCase());
});
};
/** Reset popover positioning and close. */
function _cpClosePopover(pop) {
pop.style.display = 'none';
if (pop.classList.contains('cp-fixed')) {
pop.classList.remove('cp-fixed');
pop.style.position = '';
pop.style.top = '';
pop.style.right = '';
pop.style.left = '';
}
const card = pop.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated');
}
window._cpPick = function (id, hex) {
// Update swatch
const swatch = document.getElementById(`cp-swatch-${id}`);
@@ -103,16 +130,14 @@ window._cpPick = function (id, hex) {
// Update native input
const native = document.getElementById(`cp-native-${id}`);
if (native) native.value = hex;
// Mark active dot
// Mark active dot and close
const pop = document.getElementById(`cp-pop-${id}`);
if (pop) {
pop.querySelectorAll('.color-picker-dot').forEach(d => {
const dHex = _rgbToHex(d.style.backgroundColor || d.style.background);
d.classList.toggle('active', dHex.toLowerCase() === hex.toLowerCase());
});
pop.style.display = 'none';
const card = pop.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated');
_cpClosePopover(pop);
}
// Fire callback
if (_callbacks[id]) _callbacks[id](hex);
@@ -126,20 +151,14 @@ window._cpReset = function (id, resetColor) {
const pop = document.getElementById(`cp-pop-${id}`);
if (pop) {
pop.querySelectorAll('.color-picker-dot').forEach(d => d.classList.remove('active'));
pop.style.display = 'none';
const card = pop.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated');
_cpClosePopover(pop);
}
// Fire callback with empty string to signal removal
if (_callbacks[id]) _callbacks[id]('');
};
export function closeAllColorPickers() {
document.querySelectorAll('.color-picker-popover').forEach(p => {
p.style.display = 'none';
const card = p.closest('.card, .template-card');
if (card) card.classList.remove('cp-elevated');
});
document.querySelectorAll('.color-picker-popover').forEach(p => _cpClosePopover(p));
}
// Close on outside click

View File

@@ -25,6 +25,12 @@ export function openDisplayPicker(callback, selectedIndex, engineType = null) {
_pickerEngineType = engineType || null;
const lightbox = document.getElementById('display-picker-lightbox');
// Use "Select a Device" title for engines with own display lists (camera, scrcpy, etc.)
const titleEl = lightbox.querySelector('.display-picker-title');
if (titleEl) {
titleEl.textContent = t(_pickerEngineType ? 'displays.picker.title.device' : 'displays.picker.title');
}
lightbox.classList.add('active');
requestAnimationFrame(() => {

View File

@@ -1295,8 +1295,8 @@ function renderPictureSourcesList(streams) {
];
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.count}</span></button>`
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllStreamSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} <span data-i18n="${tab.titleKey}">${t(tab.titleKey)}</span> <span class="stream-tab-count">${tab.count}</span></button>`
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllStreamSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
const renderAudioSourceCard = (src) => {
const isMono = src.source_type === 'mono';

View File

@@ -569,8 +569,8 @@ export async function loadTargetsTab() {
];
const tabBar = `<div class="stream-tab-bar">${subTabs.map(tab =>
`<button class="target-sub-tab-btn stream-tab-btn${tab.key === activeSubTab ? ' active' : ''}" data-target-sub-tab="${tab.key}" onclick="switchTargetSubTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.count}</span></button>`
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllTargetSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllTargetSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
`<button class="target-sub-tab-btn stream-tab-btn${tab.key === activeSubTab ? ' active' : ''}" data-target-sub-tab="${tab.key}" onclick="switchTargetSubTab('${tab.key}')">${tab.icon} <span data-i18n="${tab.titleKey}">${t(tab.titleKey)}</span> <span class="stream-tab-count">${tab.count}</span></button>`
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllTargetSections()" data-i18n-title="section.expand_all" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllTargetSections()" data-i18n-title="section.collapse_all" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startTargetsTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
// Use window.createPatternTemplateCard to avoid circular import
const createPatternTemplateCard = window.createPatternTemplateCard || (() => '');

View File

@@ -38,6 +38,7 @@
"displays.none": "No displays available",
"displays.failed": "Failed to load displays",
"displays.picker.title": "Select a Display",
"displays.picker.title.device": "Select a Device",
"displays.picker.select": "Select display...",
"displays.picker.click_to_select": "Click to select this display",
"displays.picker.adb_connect": "Connect ADB device",

View File

@@ -38,6 +38,7 @@
"displays.none": "Нет доступных дисплеев",
"displays.failed": "Не удалось загрузить дисплеи",
"displays.picker.title": "Выберите Дисплей",
"displays.picker.title.device": "Выберите Устройство",
"displays.picker.select": "Выберите дисплей...",
"displays.picker.click_to_select": "Нажмите, чтобы выбрать этот дисплей",
"displays.picker.adb_connect": "Подключить ADB устройство",

View File

@@ -38,6 +38,7 @@
"displays.none": "没有可用的显示器",
"displays.failed": "加载显示器失败",
"displays.picker.title": "选择显示器",
"displays.picker.title.device": "选择设备",
"displays.picker.select": "选择显示器...",
"displays.picker.click_to_select": "点击选择此显示器",
"displays.picker.adb_connect": "连接 ADB 设备",