diff --git a/ui_server.py b/ui_server.py
index ea4f561..72435b6 100644
--- a/ui_server.py
+++ b/ui_server.py
@@ -1287,8 +1287,8 @@ HTML = """
.config-scale-control input { width: 74px; height: 16px; margin: 0; accent-color: var(--ui-accent); cursor: pointer; }
.config-scale-control input:disabled { opacity: .45; cursor: default; }
.config-scale-control output { min-width: 34px; color: var(--ui-text); font-variant-numeric: tabular-nums; text-align: right; }
- .config-scale-mode { min-height: 26px !important; padding: 0 8px !important; white-space: nowrap; }
- .config-scale-mode[aria-pressed="true"] { color: #fff !important; background: var(--ui-accent-bg) !important; border-color: var(--ui-accent) !important; box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-accent) 16%, transparent); }
+ .block-scale-toggle { flex: 0 0 auto !important; width: auto !important; min-width: 48px !important; min-height: 23px !important; height: 23px !important; margin-left: auto; padding: 0 6px !important; border: 1px solid var(--ui-border) !important; border-radius: 6px !important; background: color-mix(in srgb, var(--ui-accent) 8%, var(--ui-panel)) !important; color: var(--ui-muted) !important; font: 800 9px/1 ui-sans-serif, system-ui, sans-serif !important; letter-spacing: .02em; cursor: pointer !important; }
+ .block-scale-toggle:hover, .block-scale-toggle[aria-pressed="true"] { color: #fff !important; background: var(--ui-accent-bg) !important; border-color: var(--ui-accent) !important; }
.config-layout-state { min-width: 0; color: var(--ui-muted); font-size: 10px; }
.config-workspace { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; min-height: 0; }
.config-workspace.has-structure { grid-template-columns: minmax(190px, 260px) minmax(0, 1fr); align-items: start; }
@@ -1329,7 +1329,7 @@ HTML = """
.config-canvas > .config-custom-block { border-style: dashed !important; }
.config-block > summary { position: sticky; top: 0; z-index: 2; background: color-mix(in srgb, var(--ui-panel) 92%, var(--ui-accent)); border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); }
.config-block > summary::after { display: none !important; }
- .block-title { min-width: 0; display: inline-flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+ .block-title { min-width: 0; flex: 1 1 auto; display: inline-flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.block-title::before { content: "✦"; flex: 0 0 auto; color: var(--ui-accent); font-size: .85em; opacity: .8; }
.config-block > summary { cursor: grab; touch-action: none; }
.config-block > summary:active, .config-block.is-dragging > summary { cursor: grabbing; }
@@ -1496,6 +1496,8 @@ HTML = """
.network-custom-block { border-style: dashed !important; }
.network-block.is-deleted, .network-block.is-hidden, .network-canvas > .network-block[data-block-state="hidden"], .network-canvas > .network-block[data-block-state="deleted"] { display: none !important; }
.network-summary { display: flex; align-items: center; gap: 6px; margin: -2px 0 8px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); color: var(--ui-text); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
+ .network-summary .block-title::before { display: none; }
+ .network-summary .block-scale-toggle { margin-left: auto; }
.network-summary::before { content: "✦"; color: var(--ui-accent); font-size: 11px; }
.network-block.settings-group { --block-scale: 1; padding: calc(14px * var(--block-scale)); gap: calc(8px * var(--block-scale)); }
.network-block.settings-group label, .network-block.settings-group .model-state { gap: calc(3px * var(--block-scale)); font-size: clamp(8px, calc(11px * var(--block-scale)), 22px); }
@@ -1605,7 +1607,6 @@ HTML = """
-
Ctrl/Cmd + клик — выбрать несколько · перетаскивайте группу
@@ -1832,7 +1833,6 @@ HTML = """
-
Ctrl/Cmd + клик — выбрать несколько · перетаскивайте группу
@@ -2097,7 +2097,6 @@ HTML = """
let deletedPanelOpen = false;
let configZIndex = 1;
let configContentScale = 1;
- let configScaleWheelEnabled = false;
let configBlockObserver = null;
let selectedConfigBlock = null;
let selectedConfigBlocks = new Set();
@@ -2141,15 +2140,16 @@ HTML = """
setValue('configContentScale', Math.round(configContentScale * 100));
setText('configContentScaleValue', `${Math.round(configContentScale * 100)}%`);
updateConfigBlockScale(block);
+ updateConfigScaleToggle(block);
saveConfigLayout();
}
- function setConfigScaleMode(enabled) {
- configScaleWheelEnabled = !!enabled && !!selectedConfigBlock;
- const button = $('configScaleMode');
+ function updateConfigScaleToggle(block) {
+ const button = block?.querySelector(':scope > summary .block-scale-toggle');
if (!button) return;
- button.disabled = !selectedConfigBlock;
- button.setAttribute('aria-pressed', String(configScaleWheelEnabled));
- button.textContent = configScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл';
+ const value = Math.round(Number(configLayout[block.dataset.section]?.scale) || 100);
+ button.textContent = `↕ ${value}%`;
+ button.setAttribute('aria-pressed', String(!!block._configScaleWheelEnabled));
+ button.title = block._configScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока';
}
function selectConfigBlock(block) {
selectedConfigBlocks = new Set(block && !configBlockState(block) ? [block] : []);
@@ -2165,7 +2165,6 @@ HTML = """
if (!selectedConfigBlock) {
if (control) control.disabled = true;
if (remove) remove.disabled = true;
- setConfigScaleMode(false);
return;
}
const entry = configLayout[selectedConfigBlock.dataset.section] || {};
@@ -2175,7 +2174,6 @@ HTML = """
setText('configContentScaleValue', `${value}%`);
if (control) control.disabled = selectedConfigBlocks.size !== 1;
if (remove) remove.disabled = !selectedConfigBlocks.size;
- setConfigScaleMode(configScaleWheelEnabled);
updateConfigBlockScale(selectedConfigBlock);
if (selectedConfigBlocks.size > 1) setText('configLayoutState', `выбрано ${selectedConfigBlocks.size} блока · перетаскивайте группу`);
}
@@ -2576,7 +2574,21 @@ HTML = """
const titleNode = document.createElement('span');
titleNode.className = 'block-title';
titleNode.textContent = title;
- summary.appendChild(titleNode);
+ const scaleToggle = document.createElement('button');
+ scaleToggle.className = 'block-scale-toggle';
+ scaleToggle.type = 'button';
+ scaleToggle.setAttribute('aria-label', `Масштаб блока «${title}» колесом`);
+ scaleToggle.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); });
+ scaleToggle.addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+ selectConfigBlock(block);
+ bringConfigBlockFront(block);
+ block._configScaleWheelEnabled = !block._configScaleWheelEnabled;
+ updateConfigScaleToggle(block);
+ });
+ summary.append(titleNode, scaleToggle);
+ updateConfigScaleToggle(block);
summary.setAttribute('aria-label', `${title}. Перетаскивайте заголовок для перемещения`);
summary.setAttribute('aria-expanded', 'true');
summary.addEventListener('click', event => {
@@ -2653,7 +2665,7 @@ HTML = """
}
});
block.addEventListener('wheel', event => {
- if (!configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return;
+ if (!block._configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return;
event.preventDefault();
event.stopPropagation();
if (selectedConfigBlock !== block) selectConfigBlock(block);
@@ -2779,7 +2791,6 @@ HTML = """
selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null);
$('configResetLayout')?.addEventListener('click', resetConfigLayout);
$('configDeleteSelected')?.addEventListener('click', deleteSelectedConfigBlocks);
- $('configScaleMode')?.addEventListener('click', () => setConfigScaleMode(!configScaleWheelEnabled));
$('configRestoreToggle')?.addEventListener('click', () => {
restorePanelOpen = !restorePanelOpen;
renderConfigRestorePanel();
@@ -2835,7 +2846,6 @@ HTML = """
let selectedNetworkBlocks = new Set();
let networkStructureOpen = false;
let networkContentScale = 1;
- let networkScaleWheelEnabled = false;
function readNetworkLayout() {
try {
const saved = JSON.parse(localStorage.getItem('fpv-network-layout-v1') || '{}');
@@ -2851,7 +2861,11 @@ HTML = """
function networkMinWidth() { return window.innerWidth <= 760 ? 220 : 240; }
function networkCanvasWidth() { return Math.max(networkMinWidth(), (networkCanvas?.clientWidth || 0) - 28); }
function networkState(block) { return block?.dataset.blockState || ''; }
- function networkTitle(block) { return block?.querySelector(':scope > .network-summary')?.textContent?.trim() || block?.dataset.section || 'Блок'; }
+ function networkTitle(block) {
+ const summary = block?.querySelector(':scope > .network-summary');
+ const direct = summary ? [...summary.childNodes].filter(node => node.nodeType === Node.TEXT_NODE).map(node => node.textContent).join('').trim() : '';
+ return block?.querySelector(':scope > .network-summary .block-title')?.textContent?.trim() || direct || block?.dataset.section || 'Блок';
+ }
function networkTemplateKey(block) { return block?.dataset.custom === 'true' ? (block.dataset.template || 'model') : block?.dataset.section; }
function nextNetworkKey() {
let index = 1;
@@ -2882,7 +2896,6 @@ HTML = """
if (!networkSelectedBlock) {
if (control) control.disabled = true;
if (remove) remove.disabled = true;
- setNetworkScaleMode(false);
return;
}
const value = Math.max(50, Math.min(200, Math.round(Number(networkLayout[networkSelectedBlock.dataset.section]?.scale) || 100)));
@@ -2891,7 +2904,6 @@ HTML = """
setText('networkContentScaleValue', `${value}%`);
if (control) control.disabled = selectedNetworkBlocks.size !== 1;
if (remove) remove.disabled = !selectedNetworkBlocks.size;
- setNetworkScaleMode(networkScaleWheelEnabled);
updateNetworkBlockScale(networkSelectedBlock);
if (selectedNetworkBlocks.size > 1) setText('networkLayoutState', `выбрано ${selectedNetworkBlocks.size} блока · перетаскивайте группу`);
}
@@ -2912,15 +2924,16 @@ HTML = """
networkLayout[block.dataset.section] = entry;
setText('networkContentScaleValue', `${Math.round(networkContentScale * 100)}%`);
updateNetworkBlockScale(block);
+ updateNetworkScaleToggle(block);
saveNetworkLayout();
}
- function setNetworkScaleMode(enabled) {
- networkScaleWheelEnabled = !!enabled && !!networkSelectedBlock;
- const button = $('networkScaleMode');
+ function updateNetworkScaleToggle(block) {
+ const button = block?.querySelector(':scope > .network-summary .block-scale-toggle');
if (!button) return;
- button.disabled = !networkSelectedBlock;
- button.setAttribute('aria-pressed', String(networkScaleWheelEnabled));
- button.textContent = networkScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл';
+ const value = Math.round(Number(networkLayout[block.dataset.section]?.scale) || 100);
+ button.textContent = `↕ ${value}%`;
+ button.setAttribute('aria-pressed', String(!!block._networkScaleWheelEnabled));
+ button.title = block._networkScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока';
}
function captureNetworkLayout(block) {
const key = block?.dataset.section;
@@ -3233,6 +3246,29 @@ HTML = """
function makeNetworkBlockInteractive(block) {
if (!block || block._networkInteractive) return;
block._networkInteractive = true;
+ const summary = block.querySelector(':scope > .network-summary');
+ if (summary) {
+ const title = summary.textContent.trim();
+ summary.textContent = '';
+ const titleNode = document.createElement('span');
+ titleNode.className = 'block-title';
+ titleNode.textContent = title;
+ const scaleToggle = document.createElement('button');
+ scaleToggle.className = 'block-scale-toggle';
+ scaleToggle.type = 'button';
+ scaleToggle.setAttribute('aria-label', `Масштаб блока «${networkTitle(block)}» колесом`);
+ scaleToggle.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); });
+ scaleToggle.addEventListener('click', event => {
+ event.preventDefault();
+ event.stopPropagation();
+ selectNetworkBlock(block);
+ bringNetworkBlockFront(block);
+ block._networkScaleWheelEnabled = !block._networkScaleWheelEnabled;
+ updateNetworkScaleToggle(block);
+ });
+ summary.append(titleNode, scaleToggle);
+ updateNetworkScaleToggle(block);
+ }
block.addEventListener('pointerdown', event => {
const additive = event.ctrlKey || event.metaKey;
const edge = networkResizeEdge(event, block);
@@ -3297,7 +3333,7 @@ HTML = """
}
});
block.addEventListener('wheel', event => {
- if (!networkScaleWheelEnabled || networkState(block) || !event.deltaY) return;
+ if (!block._networkScaleWheelEnabled || networkState(block) || !event.deltaY) return;
event.preventDefault();
event.stopPropagation();
if (networkSelectedBlock !== block) selectNetworkBlock(block);
@@ -3357,7 +3393,6 @@ HTML = """
selectNetworkBlock(networkBlocks.find(block => !networkState(block)) || null);
$('networkResetLayout')?.addEventListener('click', resetNetworkLayout);
$('networkDeleteSelected')?.addEventListener('click', deleteSelectedNetworkBlocks);
- $('networkScaleMode')?.addEventListener('click', () => setNetworkScaleMode(!networkScaleWheelEnabled));
$('networkStructureToggle')?.addEventListener('click', () => {
networkStructureOpen = !networkStructureOpen;
renderNetworkStructure();