@ -1287,8 +1287,8 @@ HTML = """<!doctype html>
. config - scale - control input { width : 74 px ; height : 16 px ; margin : 0 ; accent - color : var ( - - ui - accent ) ; cursor : pointer ; }
. config - scale - control input : disabled { opacity : .45 ; cursor : default ; }
. config - scale - control output { min - width : 34 px ; color : var ( - - ui - text ) ; font - variant - numeric : tabular - nums ; text - align : right ; }
. config- scale - mode { min - height : 26 px ! important ; padding : 0 8 px ! important ; white - space : nowrap ; }
. config- scale - mod e[ 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 : 48 px ! important ; min - height : 23 px ! important ; height : 23 px ! important ; margin - left : auto ; padding : 0 6 px ! important ; border : 1 px solid var ( - - ui - border ) ! important ; border - radius : 6 px ! important ; background : color - mix ( in srgb , var ( - - ui - accent ) 8 % , var ( - - ui - panel ) ) ! important ; color : var ( - - ui - muted ) ! important ; font : 800 9 px / 1 ui - sans - serif , system - ui , sans - serif ! important ; letter - spacing : .02 em ; cursor : pointer ! important ; }
. block- scale - toggle : hover , . block - scale - toggl e[ 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 : 10 px ; }
. config - workspace { display : grid ; grid - template - columns : minmax ( 0 , 1 fr ) ; min - width : 0 ; min - height : 0 ; }
. config - workspace . has - structure { grid - template - columns : minmax ( 190 px , 260 px ) minmax ( 0 , 1 fr ) ; align - items : start ; }
@ -1329,7 +1329,7 @@ HTML = """<!doctype 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 : 1 px 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 : 6 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. block - title { min - width : 0 ; flex: 1 1 auto ; display: inline - flex ; align - items : center ; gap : 6 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. block - title : : before { content : " ✦ " ; flex : 0 0 auto ; color : var ( - - ui - accent ) ; font - size : .85 em ; 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 = """<!doctype 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 : 6 px ; margin : - 2 px 0 8 px ; padding - bottom : 8 px ; border - bottom : 1 px solid color - mix ( in srgb , var ( - - ui - border ) 80 % , transparent ) ; color : var ( - - ui - text ) ; font - size : 12 px ; font - weight : 800 ; letter - spacing : .08 em ; 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 : 11 px ; }
. network - block . settings - group { - - block - scale : 1 ; padding : calc ( 14 px * var ( - - block - scale ) ) ; gap : calc ( 8 px * var ( - - block - scale ) ) ; }
. network - block . settings - group label , . network - block . settings - group . model - state { gap : calc ( 3 px * var ( - - block - scale ) ) ; font - size : clamp ( 8 px , calc ( 11 px * var ( - - block - scale ) ) , 22 px ) ; }
@ -1605,7 +1607,6 @@ HTML = """<!doctype html>
< button id = " configResetLayout " type = " button " > сбросить раскладку < / button >
< button id = " configRestoreToggle " type = " button " aria - expanded = " false " > структура < / 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 = " Масштаб элементов и текста выбранного блока: 50– 200 % " > < 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 >
< / div >
@ -1832,7 +1833,6 @@ HTML = """<!doctype html>
< button id = " networkResetLayout " type = " button " > сбросить раскладку < / button >
< button id = " networkStructureToggle " type = " button " aria - expanded = " false " > структура < / 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 >
< span id = " networkLayoutState " class = " config-layout-state " > Ctrl / Cmd + клик — выбрать несколько · перетаскивайте группу < / span >
< / div >
@ -2097,7 +2097,6 @@ HTML = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 = """<!doctype 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 ( ) ;