feat(ui): add scale toggle to each block

main
AlexsandrSnytkin 6 hours ago
parent cd2fdf2ae1
commit 8ca6eddc87

@ -1287,8 +1287,8 @@ HTML = """<!doctype html>
.config-scale-control input { width: 74px; height: 16px; margin: 0; accent-color: var(--ui-accent); cursor: pointer; } .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 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-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; } .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; }
.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: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-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 { 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; } .config-workspace.has-structure { grid-template-columns: minmax(190px, 260px) minmax(0, 1fr); align-items: start; }
@ -1329,7 +1329,7 @@ HTML = """<!doctype html>
.config-canvas > .config-custom-block { border-style: dashed !important; } .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 { 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; } .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; } .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 { cursor: grab; touch-action: none; }
.config-block > summary:active, .config-block.is-dragging > summary { cursor: grabbing; } .config-block > summary:active, .config-block.is-dragging > summary { cursor: grabbing; }
@ -1496,6 +1496,8 @@ HTML = """<!doctype html>
.network-custom-block { border-style: dashed !important; } .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-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 { 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-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 { --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); } .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 = """<!doctype html>
<button id="configResetLayout" type="button">сбросить раскладку</button> <button id="configResetLayout" type="button">сбросить раскладку</button>
<button id="configRestoreToggle" type="button" aria-expanded="false">структура</button> <button id="configRestoreToggle" type="button" aria-expanded="false">структура</button>
<button id="configDeleteSelected" class="command danger" type="button" disabled>удалить выбранные</button> <button id="configDeleteSelected" class="command danger" type="button" disabled>удалить выбранные</button>
<button id="configScaleMode" class="config-scale-mode" type="button" aria-pressed="false" disabled>масштаб колесом: выкл</button>
<label class="config-scale-control" title="Масштаб элементов и текста выбранного блока: 50200%"><span id="configScaleLabel">масштаб блока</span><input id="configContentScale" type="range" min="50" max="200" step="5" value="100" aria-label="Масштаб содержимого выбранного блока, от 50 до 200 процентов" disabled><output id="configContentScaleValue">100%</output></label> <label class="config-scale-control" title="Масштаб элементов и текста выбранного блока: 50200%"><span id="configScaleLabel">масштаб блока</span><input id="configContentScale" type="range" min="50" max="200" step="5" value="100" aria-label="Масштаб содержимого выбранного блока, от 50 до 200 процентов" disabled><output id="configContentScaleValue">100%</output></label>
<span id="configLayoutState" class="config-layout-state">Ctrl/Cmd + клик выбрать несколько · перетаскивайте группу</span> <span id="configLayoutState" class="config-layout-state">Ctrl/Cmd + клик выбрать несколько · перетаскивайте группу</span>
</div> </div>
@ -1832,7 +1833,6 @@ HTML = """<!doctype html>
<button id="networkResetLayout" type="button">сбросить раскладку</button> <button id="networkResetLayout" type="button">сбросить раскладку</button>
<button id="networkStructureToggle" type="button" aria-expanded="false">структура</button> <button id="networkStructureToggle" type="button" aria-expanded="false">структура</button>
<button id="networkDeleteSelected" class="command danger" type="button" disabled>удалить выбранные</button> <button id="networkDeleteSelected" class="command danger" type="button" disabled>удалить выбранные</button>
<button id="networkScaleMode" class="config-scale-mode" type="button" aria-pressed="false" disabled>масштаб колесом: выкл</button>
<label class="config-scale-control" title="Масштаб элементов и текста выбранного сетевого блока"><span>масштаб блока</span><input id="networkContentScale" type="range" min="50" max="200" step="5" value="100" aria-label="Масштаб содержимого выбранного сетевого блока" disabled><output id="networkContentScaleValue">100%</output></label> <label class="config-scale-control" title="Масштаб элементов и текста выбранного сетевого блока"><span>масштаб блока</span><input id="networkContentScale" type="range" min="50" max="200" step="5" value="100" aria-label="Масштаб содержимого выбранного сетевого блока" disabled><output id="networkContentScaleValue">100%</output></label>
<span id="networkLayoutState" class="config-layout-state">Ctrl/Cmd + клик выбрать несколько · перетаскивайте группу</span> <span id="networkLayoutState" class="config-layout-state">Ctrl/Cmd + клик выбрать несколько · перетаскивайте группу</span>
</div> </div>
@ -2097,7 +2097,6 @@ HTML = """<!doctype html>
let deletedPanelOpen = false; let deletedPanelOpen = false;
let configZIndex = 1; let configZIndex = 1;
let configContentScale = 1; let configContentScale = 1;
let configScaleWheelEnabled = false;
let configBlockObserver = null; let configBlockObserver = null;
let selectedConfigBlock = null; let selectedConfigBlock = null;
let selectedConfigBlocks = new Set(); let selectedConfigBlocks = new Set();
@ -2141,15 +2140,16 @@ HTML = """<!doctype html>
setValue('configContentScale', Math.round(configContentScale * 100)); setValue('configContentScale', Math.round(configContentScale * 100));
setText('configContentScaleValue', `${Math.round(configContentScale * 100)}%`); setText('configContentScaleValue', `${Math.round(configContentScale * 100)}%`);
updateConfigBlockScale(block); updateConfigBlockScale(block);
updateConfigScaleToggle(block);
saveConfigLayout(); saveConfigLayout();
} }
function setConfigScaleMode(enabled) { function updateConfigScaleToggle(block) {
configScaleWheelEnabled = !!enabled && !!selectedConfigBlock; const button = block?.querySelector(':scope > summary .block-scale-toggle');
const button = $('configScaleMode');
if (!button) return; if (!button) return;
button.disabled = !selectedConfigBlock; const value = Math.round(Number(configLayout[block.dataset.section]?.scale) || 100);
button.setAttribute('aria-pressed', String(configScaleWheelEnabled)); button.textContent = ` ${value}%`;
button.textContent = configScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл'; button.setAttribute('aria-pressed', String(!!block._configScaleWheelEnabled));
button.title = block._configScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока';
} }
function selectConfigBlock(block) { function selectConfigBlock(block) {
selectedConfigBlocks = new Set(block && !configBlockState(block) ? [block] : []); selectedConfigBlocks = new Set(block && !configBlockState(block) ? [block] : []);
@ -2165,7 +2165,6 @@ HTML = """<!doctype html>
if (!selectedConfigBlock) { if (!selectedConfigBlock) {
if (control) control.disabled = true; if (control) control.disabled = true;
if (remove) remove.disabled = true; if (remove) remove.disabled = true;
setConfigScaleMode(false);
return; return;
} }
const entry = configLayout[selectedConfigBlock.dataset.section] || {}; const entry = configLayout[selectedConfigBlock.dataset.section] || {};
@ -2175,7 +2174,6 @@ HTML = """<!doctype html>
setText('configContentScaleValue', `${value}%`); setText('configContentScaleValue', `${value}%`);
if (control) control.disabled = selectedConfigBlocks.size !== 1; if (control) control.disabled = selectedConfigBlocks.size !== 1;
if (remove) remove.disabled = !selectedConfigBlocks.size; if (remove) remove.disabled = !selectedConfigBlocks.size;
setConfigScaleMode(configScaleWheelEnabled);
updateConfigBlockScale(selectedConfigBlock); updateConfigBlockScale(selectedConfigBlock);
if (selectedConfigBlocks.size > 1) setText('configLayoutState', `выбрано ${selectedConfigBlocks.size} блока · перетаскивайте группу`); if (selectedConfigBlocks.size > 1) setText('configLayoutState', `выбрано ${selectedConfigBlocks.size} блока · перетаскивайте группу`);
} }
@ -2576,7 +2574,21 @@ HTML = """<!doctype html>
const titleNode = document.createElement('span'); const titleNode = document.createElement('span');
titleNode.className = 'block-title'; titleNode.className = 'block-title';
titleNode.textContent = 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-label', `${title}. Перетаскивайте заголовок для перемещения`);
summary.setAttribute('aria-expanded', 'true'); summary.setAttribute('aria-expanded', 'true');
summary.addEventListener('click', event => { summary.addEventListener('click', event => {
@ -2653,7 +2665,7 @@ HTML = """<!doctype html>
} }
}); });
block.addEventListener('wheel', event => { block.addEventListener('wheel', event => {
if (!configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return; if (!block._configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (selectedConfigBlock !== block) selectConfigBlock(block); if (selectedConfigBlock !== block) selectConfigBlock(block);
@ -2779,7 +2791,6 @@ HTML = """<!doctype html>
selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null); selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null);
$('configResetLayout')?.addEventListener('click', resetConfigLayout); $('configResetLayout')?.addEventListener('click', resetConfigLayout);
$('configDeleteSelected')?.addEventListener('click', deleteSelectedConfigBlocks); $('configDeleteSelected')?.addEventListener('click', deleteSelectedConfigBlocks);
$('configScaleMode')?.addEventListener('click', () => setConfigScaleMode(!configScaleWheelEnabled));
$('configRestoreToggle')?.addEventListener('click', () => { $('configRestoreToggle')?.addEventListener('click', () => {
restorePanelOpen = !restorePanelOpen; restorePanelOpen = !restorePanelOpen;
renderConfigRestorePanel(); renderConfigRestorePanel();
@ -2835,7 +2846,6 @@ HTML = """<!doctype html>
let selectedNetworkBlocks = new Set(); let selectedNetworkBlocks = new Set();
let networkStructureOpen = false; let networkStructureOpen = false;
let networkContentScale = 1; let networkContentScale = 1;
let networkScaleWheelEnabled = false;
function readNetworkLayout() { function readNetworkLayout() {
try { try {
const saved = JSON.parse(localStorage.getItem('fpv-network-layout-v1') || '{}'); const saved = JSON.parse(localStorage.getItem('fpv-network-layout-v1') || '{}');
@ -2851,7 +2861,11 @@ HTML = """<!doctype html>
function networkMinWidth() { return window.innerWidth <= 760 ? 220 : 240; } function networkMinWidth() { return window.innerWidth <= 760 ? 220 : 240; }
function networkCanvasWidth() { return Math.max(networkMinWidth(), (networkCanvas?.clientWidth || 0) - 28); } function networkCanvasWidth() { return Math.max(networkMinWidth(), (networkCanvas?.clientWidth || 0) - 28); }
function networkState(block) { return block?.dataset.blockState || ''; } 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 networkTemplateKey(block) { return block?.dataset.custom === 'true' ? (block.dataset.template || 'model') : block?.dataset.section; }
function nextNetworkKey() { function nextNetworkKey() {
let index = 1; let index = 1;
@ -2882,7 +2896,6 @@ HTML = """<!doctype html>
if (!networkSelectedBlock) { if (!networkSelectedBlock) {
if (control) control.disabled = true; if (control) control.disabled = true;
if (remove) remove.disabled = true; if (remove) remove.disabled = true;
setNetworkScaleMode(false);
return; return;
} }
const value = Math.max(50, Math.min(200, Math.round(Number(networkLayout[networkSelectedBlock.dataset.section]?.scale) || 100))); const value = Math.max(50, Math.min(200, Math.round(Number(networkLayout[networkSelectedBlock.dataset.section]?.scale) || 100)));
@ -2891,7 +2904,6 @@ HTML = """<!doctype html>
setText('networkContentScaleValue', `${value}%`); setText('networkContentScaleValue', `${value}%`);
if (control) control.disabled = selectedNetworkBlocks.size !== 1; if (control) control.disabled = selectedNetworkBlocks.size !== 1;
if (remove) remove.disabled = !selectedNetworkBlocks.size; if (remove) remove.disabled = !selectedNetworkBlocks.size;
setNetworkScaleMode(networkScaleWheelEnabled);
updateNetworkBlockScale(networkSelectedBlock); updateNetworkBlockScale(networkSelectedBlock);
if (selectedNetworkBlocks.size > 1) setText('networkLayoutState', `выбрано ${selectedNetworkBlocks.size} блока · перетаскивайте группу`); if (selectedNetworkBlocks.size > 1) setText('networkLayoutState', `выбрано ${selectedNetworkBlocks.size} блока · перетаскивайте группу`);
} }
@ -2912,15 +2924,16 @@ HTML = """<!doctype html>
networkLayout[block.dataset.section] = entry; networkLayout[block.dataset.section] = entry;
setText('networkContentScaleValue', `${Math.round(networkContentScale * 100)}%`); setText('networkContentScaleValue', `${Math.round(networkContentScale * 100)}%`);
updateNetworkBlockScale(block); updateNetworkBlockScale(block);
updateNetworkScaleToggle(block);
saveNetworkLayout(); saveNetworkLayout();
} }
function setNetworkScaleMode(enabled) { function updateNetworkScaleToggle(block) {
networkScaleWheelEnabled = !!enabled && !!networkSelectedBlock; const button = block?.querySelector(':scope > .network-summary .block-scale-toggle');
const button = $('networkScaleMode');
if (!button) return; if (!button) return;
button.disabled = !networkSelectedBlock; const value = Math.round(Number(networkLayout[block.dataset.section]?.scale) || 100);
button.setAttribute('aria-pressed', String(networkScaleWheelEnabled)); button.textContent = ` ${value}%`;
button.textContent = networkScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл'; button.setAttribute('aria-pressed', String(!!block._networkScaleWheelEnabled));
button.title = block._networkScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока';
} }
function captureNetworkLayout(block) { function captureNetworkLayout(block) {
const key = block?.dataset.section; const key = block?.dataset.section;
@ -3233,6 +3246,29 @@ HTML = """<!doctype html>
function makeNetworkBlockInteractive(block) { function makeNetworkBlockInteractive(block) {
if (!block || block._networkInteractive) return; if (!block || block._networkInteractive) return;
block._networkInteractive = true; 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 => { block.addEventListener('pointerdown', event => {
const additive = event.ctrlKey || event.metaKey; const additive = event.ctrlKey || event.metaKey;
const edge = networkResizeEdge(event, block); const edge = networkResizeEdge(event, block);
@ -3297,7 +3333,7 @@ HTML = """<!doctype html>
} }
}); });
block.addEventListener('wheel', event => { block.addEventListener('wheel', event => {
if (!networkScaleWheelEnabled || networkState(block) || !event.deltaY) return; if (!block._networkScaleWheelEnabled || networkState(block) || !event.deltaY) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (networkSelectedBlock !== block) selectNetworkBlock(block); if (networkSelectedBlock !== block) selectNetworkBlock(block);
@ -3357,7 +3393,6 @@ HTML = """<!doctype html>
selectNetworkBlock(networkBlocks.find(block => !networkState(block)) || null); selectNetworkBlock(networkBlocks.find(block => !networkState(block)) || null);
$('networkResetLayout')?.addEventListener('click', resetNetworkLayout); $('networkResetLayout')?.addEventListener('click', resetNetworkLayout);
$('networkDeleteSelected')?.addEventListener('click', deleteSelectedNetworkBlocks); $('networkDeleteSelected')?.addEventListener('click', deleteSelectedNetworkBlocks);
$('networkScaleMode')?.addEventListener('click', () => setNetworkScaleMode(!networkScaleWheelEnabled));
$('networkStructureToggle')?.addEventListener('click', () => { $('networkStructureToggle')?.addEventListener('click', () => {
networkStructureOpen = !networkStructureOpen; networkStructureOpen = !networkStructureOpen;
renderNetworkStructure(); renderNetworkStructure();

Loading…
Cancel
Save