From ba503d52fdfb216f7d375455530a888eb0dcef7d Mon Sep 17 00:00:00 2001 From: AlexsandrSnytkin Date: Mon, 21 Sep 2026 16:29:38 +0700 Subject: [PATCH] feat(ui): embed block zoom controls --- ui_server.py | 82 ++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/ui_server.py b/ui_server.py index 72435b6..27da2f3 100644 --- a/ui_server.py +++ b/ui_server.py @@ -1287,8 +1287,12 @@ 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; } - .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; } + .block-zoom { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 2px; margin-left: auto; padding: 2px; border: 1px solid color-mix(in srgb, var(--ui-accent) 42%, var(--ui-border)); border-radius: 999px; background: color-mix(in srgb, var(--ui-panel) 92%, var(--ui-accent)); box-shadow: 0 3px 10px color-mix(in srgb, var(--ui-bg) 16%, transparent); } + .block-zoom button, .block-zoom output { flex: 0 0 auto !important; width: 22px !important; min-width: 22px !important; height: 20px !important; min-height: 20px !important; margin: 0 !important; padding: 0 !important; border: 0 !important; border-radius: 999px !important; background: transparent !important; color: var(--ui-muted) !important; font: 800 10px/20px ui-sans-serif, system-ui, sans-serif !important; text-align: center; } + .block-zoom output { width: 42px !important; min-width: 42px !important; color: var(--ui-text) !important; font-variant-numeric: tabular-nums; } + .block-zoom button { cursor: pointer !important; } + .block-zoom button:hover, .block-zoom button:focus-visible, .block-zoom button[aria-pressed="true"] { outline: 0; color: #fff !important; background: var(--ui-accent-bg) !important; } + .block-zoom .block-zoom-toggle { font-size: 12px !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; } @@ -1497,7 +1501,7 @@ HTML = """ .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 .block-zoom { 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); } @@ -2145,9 +2149,10 @@ HTML = """ } function updateConfigScaleToggle(block) { const button = block?.querySelector(':scope > summary .block-scale-toggle'); - if (!button) return; + const output = block?.querySelector(':scope > summary .block-zoom-value'); + if (!button || !output) return; const value = Math.round(Number(configLayout[block.dataset.section]?.scale) || 100); - button.textContent = `↕ ${value}%`; + output.textContent = `${value}%`; button.setAttribute('aria-pressed', String(!!block._configScaleWheelEnabled)); button.title = block._configScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока'; } @@ -2574,11 +2579,16 @@ HTML = """ const titleNode = document.createElement('span'); titleNode.className = 'block-title'; titleNode.textContent = title; + const zoom = document.createElement('span'); + zoom.className = 'block-zoom'; + zoom.setAttribute('role', 'group'); + zoom.setAttribute('aria-label', `Масштаб блока «${title}»`); + zoom.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); }); 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.textContent = '⤢'; + scaleToggle.setAttribute('aria-label', `Включить масштаб колесом для блока «${title}»`); scaleToggle.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); @@ -2587,7 +2597,25 @@ HTML = """ block._configScaleWheelEnabled = !block._configScaleWheelEnabled; updateConfigScaleToggle(block); }); - summary.append(titleNode, scaleToggle); + const makeStep = (label, delta) => { + const step = document.createElement('button'); + step.className = 'block-zoom-step'; + step.type = 'button'; + step.textContent = label; + step.setAttribute('aria-label', `${delta > 0 ? 'Увеличить' : 'Уменьшить'} масштаб блока`); + step.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + selectConfigBlock(block); + bringConfigBlockFront(block); + applyConfigContentScale((Number(configLayout[block.dataset.section]?.scale) || 100) + delta); + }); + return step; + }; + const output = document.createElement('output'); + output.className = 'block-zoom-value'; + zoom.append(scaleToggle, makeStep('−', -5), output, makeStep('+', 5)); + summary.append(titleNode, zoom); updateConfigScaleToggle(block); summary.setAttribute('aria-label', `${title}. Перетаскивайте заголовок для перемещения`); summary.setAttribute('aria-expanded', 'true'); @@ -2690,7 +2718,7 @@ HTML = """ const nestedSummary = target.closest('summary'); if (nestedSummary && nestedSummary !== summary) return true; if (nestedSummary === summary) return false; - return !!target.closest('input, select, textarea, button, a, [contenteditable="true"], .drop-zone, .packet-byte-map, .packet-field-row'); + return !!target.closest('input, select, textarea, button, a, [contenteditable="true"], .block-zoom, .drop-zone, .packet-byte-map, .packet-field-row'); } function startConfigBlockDrag(block, event) { if (event.button !== 0 || !configCanvas || configDragBlocked(block, event)) return; @@ -2929,9 +2957,10 @@ HTML = """ } function updateNetworkScaleToggle(block) { const button = block?.querySelector(':scope > .network-summary .block-scale-toggle'); - if (!button) return; + const output = block?.querySelector(':scope > .network-summary .block-zoom-value'); + if (!button || !output) return; const value = Math.round(Number(networkLayout[block.dataset.section]?.scale) || 100); - button.textContent = `↕ ${value}%`; + output.textContent = `${value}%`; button.setAttribute('aria-pressed', String(!!block._networkScaleWheelEnabled)); button.title = block._networkScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока'; } @@ -3171,7 +3200,7 @@ HTML = """ function networkDragBlocked(block, event) { const target = event.target; if (!(target instanceof Element)) return true; - return !!target.closest('input, select, textarea, button, a, iframe, [contenteditable="true"], .netron-panel'); + return !!target.closest('input, select, textarea, button, a, iframe, [contenteditable="true"], .block-zoom, .netron-panel'); } function networkTrashDrop(event) { const trash = $('networkTrash'); @@ -3253,11 +3282,16 @@ HTML = """ const titleNode = document.createElement('span'); titleNode.className = 'block-title'; titleNode.textContent = title; + const zoom = document.createElement('span'); + zoom.className = 'block-zoom'; + zoom.setAttribute('role', 'group'); + zoom.setAttribute('aria-label', `Масштаб блока «${title}»`); + zoom.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); }); 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.textContent = '⤢'; + scaleToggle.setAttribute('aria-label', `Включить масштаб колесом для блока «${title}»`); scaleToggle.addEventListener('click', event => { event.preventDefault(); event.stopPropagation(); @@ -3266,7 +3300,25 @@ HTML = """ block._networkScaleWheelEnabled = !block._networkScaleWheelEnabled; updateNetworkScaleToggle(block); }); - summary.append(titleNode, scaleToggle); + const makeStep = (label, delta) => { + const step = document.createElement('button'); + step.className = 'block-zoom-step'; + step.type = 'button'; + step.textContent = label; + step.setAttribute('aria-label', `${delta > 0 ? 'Увеличить' : 'Уменьшить'} масштаб блока`); + step.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + selectNetworkBlock(block); + bringNetworkBlockFront(block); + applyNetworkContentScale((Number(networkLayout[block.dataset.section]?.scale) || 100) + delta); + }); + return step; + }; + const output = document.createElement('output'); + output.className = 'block-zoom-value'; + zoom.append(scaleToggle, makeStep('−', -5), output, makeStep('+', 5)); + summary.append(titleNode, zoom); updateNetworkScaleToggle(block); } block.addEventListener('pointerdown', event => {