iobroker.mywebui 1.176.0 → 1.180.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (252) hide show
  1. package/README.md +18 -0
  2. package/admin/i18n.json +42 -0
  3. package/admin/jsonConfig.json +35 -1
  4. package/default-controls/controls/navigation/app-bar.control +1 -1
  5. package/default-controls/controls/navigation/sidebar.control +1 -1
  6. package/dist/backend/3d-editor-integration.js +1 -1
  7. package/dist/backend/main.js +1 -1
  8. package/dist/backend/protectedWrite.js +1 -1
  9. package/dist/backend/reportCsv.js +1 -1
  10. package/dist/backend/reportGenerate.js +1 -1
  11. package/dist/backend/reportOutput.js +1 -1
  12. package/dist/backend/reportPdf.js +1 -1
  13. package/dist/backend/reportRenderer.js +1 -1
  14. package/dist/backend/reportSandbox.js +1 -1
  15. package/dist/backend/reportSandboxWorker.js +1 -1
  16. package/docs/bindings.md +2 -0
  17. package/docs/permissions.md +34 -0
  18. package/docs/screens.md +13 -2
  19. package/io-package.json +1 -1
  20. package/native/index.js +2 -1
  21. package/native/mywebui_scada_engine.darwin-arm64.node +0 -0
  22. package/native/mywebui_scada_engine.darwin-x64.node +0 -0
  23. package/native/mywebui_scada_engine.linux-arm-gnueabihf.node +0 -0
  24. package/native/mywebui_scada_engine.linux-arm64-gnu.node +0 -0
  25. package/native/mywebui_scada_engine.linux-arm64-musl.node +0 -0
  26. package/native/mywebui_scada_engine.linux-x64-gnu.node +0 -0
  27. package/native/mywebui_scada_engine.linux-x64-musl.node +0 -0
  28. package/native/mywebui_scada_engine.win32-x64-msvc.node +0 -0
  29. package/package.json +1 -1
  30. package/www/3d-editor/js/Animation.js +1 -1
  31. package/www/3d-editor/js/AnimationResizer.js +1 -1
  32. package/www/3d-editor/js/Command.js +1 -1
  33. package/www/3d-editor/js/Config.js +1 -1
  34. package/www/3d-editor/js/Editor.js +1 -1
  35. package/www/3d-editor/js/EditorControls.js +1 -1
  36. package/www/3d-editor/js/GLTFImportDialog.js +1 -1
  37. package/www/3d-editor/js/History.js +1 -1
  38. package/www/3d-editor/js/Loader.js +1 -1
  39. package/www/3d-editor/js/LoaderUtils.js +1 -1
  40. package/www/3d-editor/js/Menubar.Add.js +1 -1
  41. package/www/3d-editor/js/Menubar.Edit.js +1 -1
  42. package/www/3d-editor/js/Menubar.File.js +1 -1
  43. package/www/3d-editor/js/Menubar.Help.js +1 -1
  44. package/www/3d-editor/js/Menubar.Render.js +1 -1
  45. package/www/3d-editor/js/Menubar.Status.js +1 -1
  46. package/www/3d-editor/js/Menubar.View.js +1 -1
  47. package/www/3d-editor/js/Menubar.js +1 -1
  48. package/www/3d-editor/js/Player.js +1 -1
  49. package/www/3d-editor/js/Resizer.js +1 -1
  50. package/www/3d-editor/js/Script.js +1 -1
  51. package/www/3d-editor/js/Selector.js +1 -1
  52. package/www/3d-editor/js/Sidebar.Geometry.BoxGeometry.js +1 -1
  53. package/www/3d-editor/js/Sidebar.Geometry.BufferGeometry.js +1 -1
  54. package/www/3d-editor/js/Sidebar.Geometry.CapsuleGeometry.js +1 -1
  55. package/www/3d-editor/js/Sidebar.Geometry.CircleGeometry.js +1 -1
  56. package/www/3d-editor/js/Sidebar.Geometry.CylinderGeometry.js +1 -1
  57. package/www/3d-editor/js/Sidebar.Geometry.DodecahedronGeometry.js +1 -1
  58. package/www/3d-editor/js/Sidebar.Geometry.ExtrudeGeometry.js +1 -1
  59. package/www/3d-editor/js/Sidebar.Geometry.IcosahedronGeometry.js +1 -1
  60. package/www/3d-editor/js/Sidebar.Geometry.LatheGeometry.js +1 -1
  61. package/www/3d-editor/js/Sidebar.Geometry.Modifiers.js +1 -1
  62. package/www/3d-editor/js/Sidebar.Geometry.OctahedronGeometry.js +1 -1
  63. package/www/3d-editor/js/Sidebar.Geometry.PlaneGeometry.js +1 -1
  64. package/www/3d-editor/js/Sidebar.Geometry.RingGeometry.js +1 -1
  65. package/www/3d-editor/js/Sidebar.Geometry.ShapeGeometry.js +1 -1
  66. package/www/3d-editor/js/Sidebar.Geometry.SphereGeometry.js +1 -1
  67. package/www/3d-editor/js/Sidebar.Geometry.TetrahedronGeometry.js +1 -1
  68. package/www/3d-editor/js/Sidebar.Geometry.TextGeometry.js +1 -1
  69. package/www/3d-editor/js/Sidebar.Geometry.TorusGeometry.js +1 -1
  70. package/www/3d-editor/js/Sidebar.Geometry.TorusKnotGeometry.js +1 -1
  71. package/www/3d-editor/js/Sidebar.Geometry.TubeGeometry.js +1 -1
  72. package/www/3d-editor/js/Sidebar.Geometry.js +1 -1
  73. package/www/3d-editor/js/Sidebar.Material.BooleanProperty.js +1 -1
  74. package/www/3d-editor/js/Sidebar.Material.ColorProperty.js +1 -1
  75. package/www/3d-editor/js/Sidebar.Material.ConstantProperty.js +1 -1
  76. package/www/3d-editor/js/Sidebar.Material.MapProperty.js +1 -1
  77. package/www/3d-editor/js/Sidebar.Material.NumberProperty.js +1 -1
  78. package/www/3d-editor/js/Sidebar.Material.Program.js +1 -1
  79. package/www/3d-editor/js/Sidebar.Material.RangeValueProperty.js +1 -1
  80. package/www/3d-editor/js/Sidebar.Material.js +1 -1
  81. package/www/3d-editor/js/Sidebar.Object.js +1 -1
  82. package/www/3d-editor/js/Sidebar.Project.App.js +1 -1
  83. package/www/3d-editor/js/Sidebar.Project.Materials.js +1 -1
  84. package/www/3d-editor/js/Sidebar.Project.Renderer.js +1 -1
  85. package/www/3d-editor/js/Sidebar.Project.Resources.js +1 -1
  86. package/www/3d-editor/js/Sidebar.Project.js +1 -1
  87. package/www/3d-editor/js/Sidebar.Properties.js +1 -1
  88. package/www/3d-editor/js/Sidebar.Scene.js +1 -1
  89. package/www/3d-editor/js/Sidebar.Script.js +1 -1
  90. package/www/3d-editor/js/Sidebar.Settings.History.js +1 -1
  91. package/www/3d-editor/js/Sidebar.Settings.Shortcuts.js +1 -1
  92. package/www/3d-editor/js/Sidebar.Settings.js +1 -1
  93. package/www/3d-editor/js/Sidebar.js +1 -1
  94. package/www/3d-editor/js/Storage.js +1 -1
  95. package/www/3d-editor/js/Strings.js +1 -1
  96. package/www/3d-editor/js/TextureParametersDialog.js +1 -1
  97. package/www/3d-editor/js/Toolbar.js +1 -1
  98. package/www/3d-editor/js/Viewport.Controls.js +1 -1
  99. package/www/3d-editor/js/Viewport.Info.js +1 -1
  100. package/www/3d-editor/js/Viewport.Pathtracer.js +1 -1
  101. package/www/3d-editor/js/Viewport.ViewHelper.js +1 -1
  102. package/www/3d-editor/js/Viewport.XR.js +1 -1
  103. package/www/3d-editor/js/Viewport.js +1 -1
  104. package/www/3d-editor/js/commands/AddObjectCommand.js +1 -1
  105. package/www/3d-editor/js/commands/AddScriptCommand.js +1 -1
  106. package/www/3d-editor/js/commands/Commands.js +1 -1
  107. package/www/3d-editor/js/commands/MoveObjectCommand.js +1 -1
  108. package/www/3d-editor/js/commands/MultiCmdsCommand.js +1 -1
  109. package/www/3d-editor/js/commands/RemoveObjectCommand.js +1 -1
  110. package/www/3d-editor/js/commands/RemoveScriptCommand.js +1 -1
  111. package/www/3d-editor/js/commands/SetColorCommand.js +1 -1
  112. package/www/3d-editor/js/commands/SetGeometryCommand.js +1 -1
  113. package/www/3d-editor/js/commands/SetGeometryValueCommand.js +1 -1
  114. package/www/3d-editor/js/commands/SetMaterialColorCommand.js +1 -1
  115. package/www/3d-editor/js/commands/SetMaterialCommand.js +1 -1
  116. package/www/3d-editor/js/commands/SetMaterialMapCommand.js +1 -1
  117. package/www/3d-editor/js/commands/SetMaterialRangeCommand.js +1 -1
  118. package/www/3d-editor/js/commands/SetMaterialValueCommand.js +1 -1
  119. package/www/3d-editor/js/commands/SetMaterialVectorCommand.js +1 -1
  120. package/www/3d-editor/js/commands/SetPositionCommand.js +1 -1
  121. package/www/3d-editor/js/commands/SetRotationCommand.js +1 -1
  122. package/www/3d-editor/js/commands/SetScaleCommand.js +1 -1
  123. package/www/3d-editor/js/commands/SetSceneCommand.js +1 -1
  124. package/www/3d-editor/js/commands/SetScriptValueCommand.js +1 -1
  125. package/www/3d-editor/js/commands/SetShadowValueCommand.js +1 -1
  126. package/www/3d-editor/js/commands/SetTextureParametersCommand.js +1 -1
  127. package/www/3d-editor/js/commands/SetUuidCommand.js +1 -1
  128. package/www/3d-editor/js/commands/SetValueCommand.js +1 -1
  129. package/www/dist/frontend/bundle/HistoryDirectives-657ZB4BS.js +1 -0
  130. package/www/dist/frontend/bundle/ReportViewer-FSODEVZC.js +197 -0
  131. package/www/dist/frontend/bundle/Sha256-BSH7CIPW.js +1 -1
  132. package/www/dist/frontend/bundle/chunk-6UCSEZIG.js +1 -0
  133. package/www/dist/frontend/bundle/chunk-GKVOZVKS.js +21 -0
  134. package/www/dist/frontend/bundle/chunk-KVPT4J7H.js +1 -1
  135. package/www/dist/frontend/bundle/common/IobrokerHandler.js +1 -1
  136. package/www/dist/frontend/bundle/runtime/CustomControls.js +1 -1
  137. package/www/dist/frontend/bundle/runtime/ScreenViewer.js +1 -1
  138. package/www/dist/frontend/bundle/runtime/controls.js +4 -4
  139. package/www/dist/frontend/bundle/runtime/init.js +2 -2
  140. package/www/dist/frontend/common/Access.js +1 -0
  141. package/www/dist/frontend/common/AccessRules.js +1 -0
  142. package/www/dist/frontend/common/BindingExpression.js +1 -1
  143. package/www/dist/frontend/common/BuiltinTranslations.js +1 -1
  144. package/www/dist/frontend/common/Common.globals.js +1 -1
  145. package/www/dist/frontend/common/ConfirmWrite.js +1 -1
  146. package/www/dist/frontend/common/EChartsOption.js +1 -1
  147. package/www/dist/frontend/common/HistoryDirective.js +1 -1
  148. package/www/dist/frontend/common/HistoryOptions.js +1 -1
  149. package/www/dist/frontend/common/HistoryQuery.js +1 -1
  150. package/www/dist/frontend/common/HistoryQueryViews.js +1 -1
  151. package/www/dist/frontend/common/IobrokerHandler.js +1 -1
  152. package/www/dist/frontend/common/PlantPaxResolve.js +1 -1
  153. package/www/dist/frontend/common/ReadQuality.js +1 -1
  154. package/www/dist/frontend/common/ReportAllowlist.js +1 -1
  155. package/www/dist/frontend/common/ReportSchedule.js +1 -1
  156. package/www/dist/frontend/common/ReportTheme.js +1 -1
  157. package/www/dist/frontend/common/Runtime.js +1 -1
  158. package/www/dist/frontend/common/Sha256.js +1 -1
  159. package/www/dist/frontend/common/StartScreen.js +1 -0
  160. package/www/dist/frontend/common/StateDiagnostics.js +1 -1
  161. package/www/dist/frontend/common/StateQuality.js +1 -1
  162. package/www/dist/frontend/common/WriteFeedback.js +1 -1
  163. package/www/dist/frontend/config/CommandHandling.js +1 -1
  164. package/www/dist/frontend/config/ConfigureWebcomponentDesigner.js +1 -1
  165. package/www/dist/frontend/config/ConfirmWindow.js +1 -1
  166. package/www/dist/frontend/config/DockHelper.js +1 -1
  167. package/www/dist/frontend/config/HistoryQueryWindow.js +1 -1
  168. package/www/dist/frontend/config/IobrokerWebui3DDriveEngine.js +1 -1
  169. package/www/dist/frontend/config/IobrokerWebui3DScreenEditor.js +3 -3
  170. package/www/dist/frontend/config/IobrokerWebui3DScreenPropertiesPanel.js +3 -3
  171. package/www/dist/frontend/config/IobrokerWebui3DScreenViewer.js +3 -3
  172. package/www/dist/frontend/config/IobrokerWebuiAppShell.js +3 -3
  173. package/www/dist/frontend/config/IobrokerWebuiBindingsEditor.js +1 -1
  174. package/www/dist/frontend/config/IobrokerWebuiConfirmationWrapper.js +3 -3
  175. package/www/dist/frontend/config/IobrokerWebuiControlPropertiesEditor.js +3 -3
  176. package/www/dist/frontend/config/IobrokerWebuiDynamicPropsEditor.js +1 -1
  177. package/www/dist/frontend/config/IobrokerWebuiEventAssignment.js +1 -1
  178. package/www/dist/frontend/config/IobrokerWebuiIconsView.js +3 -3
  179. package/www/dist/frontend/config/IobrokerWebuiMonacoEditor.js +2 -2
  180. package/www/dist/frontend/config/IobrokerWebuiPlantPaxTypesEditor.js +1 -1
  181. package/www/dist/frontend/config/IobrokerWebuiPropertyGrid.js +1 -1
  182. package/www/dist/frontend/config/IobrokerWebuiScreenEditor.js +1 -1
  183. package/www/dist/frontend/config/IobrokerWebuiScreensView.js +3 -3
  184. package/www/dist/frontend/config/IobrokerWebuiSecurityEditor.js +1 -1
  185. package/www/dist/frontend/config/IobrokerWebuiSignalPropertyEditor.js +1 -1
  186. package/www/dist/frontend/config/IobrokerWebuiSolutionExplorer.js +3 -3
  187. package/www/dist/frontend/config/IobrokerWebuiTranslationEditor.js +1 -1
  188. package/www/dist/frontend/config/IobrokerWebuiWidgetGallery.js +3 -3
  189. package/www/dist/frontend/config/RightDockStrip.js +1 -1
  190. package/www/dist/frontend/config/UprightOverlays.js +1 -1
  191. package/www/dist/frontend/helper/DialogHelper.js +3 -3
  192. package/www/dist/frontend/helper/EditorOfDesignItem.js +1 -1
  193. package/www/dist/frontend/helper/EsprimaHelper.js +1 -1
  194. package/www/dist/frontend/helper/Helper.js +1 -1
  195. package/www/dist/frontend/helper/LayoutHelper.js +1 -1
  196. package/www/dist/frontend/helper/XmlHelper.js +1 -1
  197. package/www/dist/frontend/helper/ZipHelper.js +1 -1
  198. package/www/dist/frontend/runtime/AnimationService.js +1 -1
  199. package/www/dist/frontend/runtime/CustomControls.js +1 -1
  200. package/www/dist/frontend/runtime/DynamicElementProperties.js +1 -1
  201. package/www/dist/frontend/runtime/EChart.js +1 -1
  202. package/www/dist/frontend/runtime/HabPanelLikeMenu.js +3 -3
  203. package/www/dist/frontend/runtime/HistoryDirectives.js +1 -1
  204. package/www/dist/frontend/runtime/NavigationShell.js +1 -1
  205. package/www/dist/frontend/runtime/ReportViewer.js +3 -3
  206. package/www/dist/frontend/runtime/RuntimeTheme.js +1 -1
  207. package/www/dist/frontend/runtime/ScreenViewer.js +2 -2
  208. package/www/dist/frontend/runtime/TranslateableText.js +2 -2
  209. package/www/dist/frontend/runtime/VisibilityService.js +1 -1
  210. package/www/dist/frontend/runtime/controls.js +1 -1
  211. package/www/dist/frontend/runtime/init.js +1 -1
  212. package/www/dist/frontend/scripting/IobrokerWebuiScriptSystem.js +1 -1
  213. package/www/dist/frontend/scripting/blockly/OpenScreen.js +1 -1
  214. package/www/dist/frontend/scripting/blockly/webuiBlocklyToolbox.js +1 -1
  215. package/www/dist/frontend/services/DynamicPropertiesHelper.js +1 -1
  216. package/www/dist/frontend/services/IobrokerWebuiBindableLocalObjectsService.js +1 -1
  217. package/www/dist/frontend/services/IobrokerWebuiBindableObjectDragDropService.js +1 -1
  218. package/www/dist/frontend/services/IobrokerWebuiBindableObjectsForPropertiesService.js +1 -1
  219. package/www/dist/frontend/services/IobrokerWebuiBindableObjectsService.js +1 -1
  220. package/www/dist/frontend/services/IobrokerWebuiConfigButtonProvider.js +1 -1
  221. package/www/dist/frontend/services/IobrokerWebuiCopyPasteService.js +1 -1
  222. package/www/dist/frontend/services/IobrokerWebuiCustomControlEventsService.js +1 -1
  223. package/www/dist/frontend/services/IobrokerWebuiCustomElementContextMenu.js +1 -1
  224. package/www/dist/frontend/services/IobrokerWebuiDemoProviderService.js +1 -1
  225. package/www/dist/frontend/services/IobrokerWebuiDynamicPropertiesService.js +1 -1
  226. package/www/dist/frontend/services/IobrokerWebuiEChartPropertiesService.js +1 -1
  227. package/www/dist/frontend/services/IobrokerWebuiEventsService.js +1 -1
  228. package/www/dist/frontend/services/IobrokerWebuiExternalDragDropService.js +1 -1
  229. package/www/dist/frontend/services/IobrokerWebuiLitPropertiesService.js +1 -1
  230. package/www/dist/frontend/services/IobrokerWebuiModelCommandService.js +1 -1
  231. package/www/dist/frontend/services/IobrokerWebuiPropertiesService.js +1 -1
  232. package/www/dist/frontend/services/IobrokerWebuiPropertyGroupsService.js +1 -1
  233. package/www/dist/frontend/services/IobrokerWebuiRefactorService.js +1 -1
  234. package/www/dist/frontend/services/IobrokerWebuiReportContextMenu.js +1 -1
  235. package/www/dist/frontend/services/IobrokerWebuiScreenContextMenu.js +1 -1
  236. package/www/dist/frontend/services/IobrokerWebuiSpecialPropertiesService.js +1 -1
  237. package/www/dist/frontend/services/IobrokerWebuiSpectrumEditorService.js +1 -1
  238. package/www/dist/frontend/services/IobrokerWebuiVisibilityPropertiesService.js +1 -1
  239. package/www/dist/frontend/services/LayoutComponentsPropertiesService.js +1 -1
  240. package/www/dist/frontend/widgets/OrthoFlowLineTool.js +1 -1
  241. package/www/dist/frontend/widgets/ReportToolbox.js +3 -3
  242. package/www/dist/frontend/widgets/customElementsObserver.js +1 -1
  243. package/www/dist/frontend/widgets/importWidgetFiles.js +1 -1
  244. package/www/dist/frontend/widgets/importWidgetFilesRuntime.js +1 -1
  245. package/www/dist/frontend/widgets/layout-components.js +1 -1
  246. package/www/dist/frontend/widgets/layout-placement-services.js +1 -1
  247. package/www/dist/frontend/widgets/testElement.js +1 -1
  248. package/www/runtime.html +28 -13
  249. package/www/dist/frontend/bundle/HistoryDirectives-7WFU2ZTM.js +0 -1
  250. package/www/dist/frontend/bundle/ReportViewer-WPWE2MW6.js +0 -197
  251. package/www/dist/frontend/bundle/chunk-CVWTTI5E.js +0 -1
  252. package/www/dist/frontend/bundle/chunk-NPDVKA2F.js +0 -21
package/README.md CHANGED
@@ -302,6 +302,24 @@ runtime.html?screenName=screen2
302
302
  Placeholder for next versions:
303
303
  ### __WORK IN PROGRESS__
304
304
  -->
305
+ ### 1.180.2 (2026-10-07)
306
+ * (runtime) a user in no ioBroker group is no longer taken as "administrator" by the browser (startup screen per group, report toolbox); callers read the user's id instead of a missing name field
307
+
308
+ ### 1.180.1 (2026-10-07)
309
+ * (runtime) mywebui sets `local_username` itself after login (the user badge no longer depends on the Global Script); getCurrentUser() called before the connection waits for it instead of failing, and a failure is no longer remembered as user "admin" / group administrator; the state details check runs after the connection is up
310
+
311
+ ### 1.180.0 (2026-10-07)
312
+ * (security) ioBroker rights are told, not silent: group rights (State read/write, sendTo …) and the object ACL (owner/group/everyone, first match as js-controller) are checked before a write/read/sendTo and the server's refusals are caught; a notice in the corner in 5 languages (access.*), write feedback shows denied, read quality marks unreadable states 🔒; subscribed values the user may not read are dropped (ioBroker sends them anyway); IOB.access(id), IOB.accessDenied; secWriteState no longer writes past the ioBroker ACL without a security model
313
+
314
+ ### 1.178.0 (2026-10-06)
315
+ * (screens) startup screen: right-click a screen → Set as startup screen (everyone) or for a user group; Settings → Startup screen (default + ordered group rules); rename/delete keep the rules in step; no startup screen and no "start" → a message instead of an error, and a link to a missing screen says so
316
+
317
+ ### 1.177.1 (2026-10-06)
318
+ * (navigation) controls in the app bar (user badge, logout, theme toggle/selector …) take the bar's colours instead of the page theme's, so they stay readable on a dark bar in light themes; app bar theme "auto" still follows the page
319
+
320
+ ### 1.177.0 (2026-10-06)
321
+ * (navigation) a sidebar runs the full height with the app bar beside it; the ☰ goes with the menu (its top corner, in the menu's colours) and cycles full → icons → hidden again; hidden, it docks in the app bar's corner; top and bottom bars keep it in their own place
322
+
305
323
  ### 1.176.0 (2026-10-06)
306
324
  * (gokturk413) feat(navigation): the sidebar has its own toggle in its header (≡‹ folds to icons, ≡› opens), as in most UIs; the app bar's ☰ only brings back a hidden menu; on a phone the toggle hides the menu
307
325
 
package/admin/i18n.json CHANGED
@@ -182,5 +182,47 @@
182
182
  "tr": "1. Adaptörü başlatın: 14 günlük deneme otomatik başlar\n2. Yukarıdaki Hardware ID satırının tamamını kopyalayın\n3. Lisans anahtarı almak için gokturk413'e gönderin\n4. Anahtarı buraya girip kaydedin (adaptör yeniden başlar)\n\n💡 Deneme süresi anahtarsız biterse runtime durur. Deneme sırasında yapılan proje, anahtar girildikten sonra çalışmaya devam eder.\n💡 Yeni bir bilgisayar için onun Hardware ID'sini gönderip ayrı bir anahtar alın.",
183
183
  "ru": "1. Запустите адаптер: 14-дневный пробный период начнётся автоматически\n2. Скопируйте всю строку Hardware ID выше\n3. Отправьте её gokturk413, чтобы получить лицензионный ключ\n4. Введите ключ здесь и сохраните (адаптер перезапустится)\n\n💡 Если пробный период закончится без ключа, runtime останавливается. Проект, созданный в пробный период, продолжит работать после ввода ключа.\n💡 Для нового компьютера отправьте его Hardware ID и получите для него ключ.",
184
184
  "de": "1. Adapter starten: eine 14-tägige Testphase beginnt automatisch\n2. Die ganze Hardware-ID-Zeile oben kopieren\n3. An gokturk413 senden, um einen Lizenzschlüssel zu erhalten\n4. Den Schlüssel hier eingeben und speichern (der Adapter startet neu)\n\n💡 Endet die Testphase ohne Schlüssel, stoppt die Runtime. Ein in der Testphase erstelltes Projekt läuft nach Eingabe des Schlüssels weiter.\n💡 Für einen neuen Computer dessen Hardware-ID senden und einen eigenen Schlüssel erhalten."
185
+ },
186
+ "Offline activation": {
187
+ "en": "Offline activation",
188
+ "az": "Oflayn aktivləşdirmə",
189
+ "tr": "Çevrimdışı etkinleştirme",
190
+ "ru": "Офлайн-активация",
191
+ "de": "Offline-Aktivierung"
192
+ },
193
+ "No internet on site? Create an activation request, send it to gokturk413 (email, USB), and paste the key or the whole response file you get back into the License Key field above.": {
194
+ "en": "No internet on site? Create an activation request, send it to gokturk413 (email, USB), and paste the key or the whole response file you get back into the License Key field above.",
195
+ "az": "Obyektdə internet yoxdur? Aktivləşdirmə sorğusu yaradın, gokturk413-ə göndərin (e-poçt, USB) və geri gələn açarı və ya cavab faylının hamısını yuxarıdakı License Key sahəsinə yapışdırın.",
196
+ "tr": "Sahada internet yok mu? Bir etkinleştirme isteği oluşturun, gokturk413'e gönderin (e-posta, USB) ve gelen anahtarı veya yanıt dosyasının tamamını yukarıdaki License Key alanına yapıştırın.",
197
+ "ru": "Нет интернета на объекте? Создайте запрос активации, отправьте его gokturk413 (почта, USB) и вставьте полученный ключ или весь файл ответа в поле License Key выше.",
198
+ "de": "Kein Internet vor Ort? Aktivierungsanfrage erstellen, an gokturk413 senden (E-Mail, USB) und den erhaltenen Schlüssel oder die ganze Antwortdatei oben in das Feld License Key einfügen."
199
+ },
200
+ "Create activation request": {
201
+ "en": "Create activation request",
202
+ "az": "Aktivləşdirmə sorğusu yarat",
203
+ "tr": "Etkinleştirme isteği oluştur",
204
+ "ru": "Создать запрос активации",
205
+ "de": "Aktivierungsanfrage erstellen"
206
+ },
207
+ "Activation request": {
208
+ "en": "Activation request",
209
+ "az": "Aktivləşdirmə sorğusu",
210
+ "tr": "Etkinleştirme isteği",
211
+ "ru": "Запрос активации",
212
+ "de": "Aktivierungsanfrage"
213
+ },
214
+ "Copy this line (copy button) and send it. It holds the Hardware IDs of this computer, the environment (physical, VM or container) and the host name; it contains no secret.": {
215
+ "en": "Copy this line (copy button) and send it. It holds the Hardware IDs of this computer, the environment (physical, VM or container) and the host name; it contains no secret.",
216
+ "az": "Bu sətri kopyalayın (kopyalama düyməsi) və göndərin. İçində bu kompüterin Hardware ID-ləri, mühit (fiziki, VM və ya konteyner) və host adı var; gizli məlumat yoxdur.",
217
+ "tr": "Bu satırı kopyalayın (kopyala düğmesi) ve gönderin. Bu bilgisayarın Hardware ID'lerini, ortamı (fiziksel, VM veya konteyner) ve ana bilgisayar adını içerir; gizli bilgi içermez.",
218
+ "ru": "Скопируйте эту строку (кнопка копирования) и отправьте. В ней Hardware ID этого компьютера, среда (физическая, ВМ или контейнер) и имя хоста; секретов нет.",
219
+ "de": "Diese Zeile kopieren (Kopier-Schaltfläche) und senden. Sie enthält die Hardware-IDs dieses Computers, die Umgebung (physisch, VM oder Container) und den Hostnamen; keine Geheimnisse."
220
+ },
221
+ "Paste the licence key, or the whole activation response file. Contact gokturk413 for a key.": {
222
+ "en": "Paste the licence key, or the whole activation response file. Contact gokturk413 for a key.",
223
+ "az": "Lisenziya açarını və ya aktivləşdirmə cavab faylının hamısını yapışdırın. Açar üçün gokturk413 ilə əlaqə saxlayın.",
224
+ "tr": "Lisans anahtarını veya etkinleştirme yanıt dosyasının tamamını yapıştırın. Anahtar için gokturk413 ile iletişime geçin.",
225
+ "ru": "Вставьте лицензионный ключ или весь файл ответа активации. За ключом обращайтесь к gokturk413.",
226
+ "de": "Lizenzschlüssel oder die ganze Aktivierungs-Antwortdatei einfügen. Schlüssel bei gokturk413 anfragen."
185
227
  }
186
228
  }
@@ -26,7 +26,7 @@
26
26
  "sm": 12,
27
27
  "md": 8,
28
28
  "lg": 6,
29
- "help": "Enter your license key here. Contact gokturk413 for a valid license key.",
29
+ "help": "Paste the licence key, or the whole activation response file. Contact gokturk413 for a key.",
30
30
  "visible": true,
31
31
  "repeat": false
32
32
  },
@@ -82,6 +82,40 @@
82
82
  "borderRadius": "4px"
83
83
  }
84
84
  },
85
+ "_activationHeader": {
86
+ "type": "header",
87
+ "text": "Offline activation",
88
+ "size": 4
89
+ },
90
+ "_activationInfo": {
91
+ "type": "staticText",
92
+ "text": "No internet on site? Create an activation request, send it to gokturk413 (email, USB), and paste the key or the whole response file you get back into the License Key field above.",
93
+ "style": {
94
+ "fontSize": 13,
95
+ "marginBottom": 8
96
+ }
97
+ },
98
+ "activationRequest": {
99
+ "type": "text",
100
+ "label": "Activation request",
101
+ "readOnly": true,
102
+ "copyToClipboard": true,
103
+ "sm": 12,
104
+ "md": 8,
105
+ "lg": 8,
106
+ "help": "Copy this line (copy button) and send it. It holds the Hardware IDs of this computer, the environment (physical, VM or container) and the host name; it contains no secret."
107
+ },
108
+ "_getActivationBtn": {
109
+ "type": "sendTo",
110
+ "label": "Create activation request",
111
+ "command": "getActivationRequest",
112
+ "jsonData": "{}",
113
+ "variant": "contained",
114
+ "icon": "send",
115
+ "useNative": true,
116
+ "sm": 12,
117
+ "md": 4
118
+ },
85
119
  "_separator": {
86
120
  "type": "divider"
87
121
  },
@@ -1 +1 @@
1
- {"properties": {"caption": {"type": "string", "default": ""}, "icon": {"type": "string", "default": ""}, "bgColor": {"type": "color", "default": ""}}, "settings": {"width": "100%", "height": "48px"}, "html": "<div id=\"bar\" class=\"bar\"><div id=\"slotLeft\" class=\"bar-slot\"></div><img id=\"barIcon\" class=\"bar-icon\" alt=\"\" style=\"display:none;\"/><span id=\"barCap\" class=\"bar-cap\"></span><div id=\"slotCenter\" class=\"bar-slot bar-grow\" style=\"justify-content:center;\"></div><div id=\"slotRight\" class=\"bar-slot\"></div><button id=\"barMore\" class=\"bar-more\" type=\"button\" aria-label=\"More\" aria-expanded=\"false\">⋮</button></div>", "style": ":host{display:block;width:100%;height:100%;}\n.bar{display:flex;align-items:center;gap:12px;height:100%;padding:var(--bar-pad,0 16px 0 60px);color:var(--bar-fg,#fff);box-sizing:border-box;background:var(--bar-bg,linear-gradient(90deg,#1e3c72 0%,#2a5298 100%));box-shadow:0 2px 12px rgba(0,0,0,.25);-webkit-backdrop-filter:var(--ui-backdrop,none);backdrop-filter:var(--ui-backdrop,none);}\n.bar-icon{height:30px;width:auto;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3));}\n.bar-cap{font-size:17px;font-weight:600;letter-spacing:.3px;}\n.bar-slot{display:flex;align-items:center;gap:8px;}\n.bar-grow{flex:1;min-width:0;}\n:host([data-theme=\"dark\"]){--bar-bg:linear-gradient(90deg,#0a0e27 0%,#1a1f3a 100%);}\n:host([data-theme=\"light\"]){--bar-bg:linear-gradient(90deg,#ffffff 0%,#eceff1 100%);}\n:host([data-theme=\"purple\"]){--bar-bg:linear-gradient(90deg,#4a1d6e 0%,#7b2d8e 100%);}\n:host([data-theme=\"green\"]){--bar-bg:linear-gradient(90deg,#1b5e20 0%,#2e7d32 100%);}\n:host([data-theme=\"orange\"]){--bar-bg:linear-gradient(90deg,#bf360c 0%,#e64a19 100%);}\n:host([data-theme=\"teal\"]){--bar-bg:linear-gradient(90deg,#004d40 0%,#00695c 100%);}\n:host([data-theme=\"auto\"]){--bar-bg:var(--ui-surface,#252b38);--bar-fg:var(--ui-text,#e2e6ec);}\n.bar-cap{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.bar-more{display:none;flex:none;margin-left:auto;width:40px;height:40px;border:0;border-radius:10px;background:transparent;color:inherit;font:700 22px/1 sans-serif;cursor:pointer;}\n.bar-more:hover,.bar-more[aria-expanded=\"true\"]{background:rgba(255,255,255,.14);}\n.bar-more:focus-visible{outline:2px solid currentColor;outline-offset:2px;}\n/* a phone: the centre and right slots move into a panel the ⋮ opens */\n@media (max-width:768px){\n .bar{gap:8px;}\n .bar-cap{font-size:15px;}\n .bar-more{display:block;}\n #slotCenter,#slotRight{display:none;position:fixed;right:8px;top:var(--bar-panel-top,52px);z-index:100010;flex-direction:column;align-items:stretch;gap:10px;\n width:min(320px,calc(100vw - 16px));max-height:calc(100vh - var(--bar-panel-top,52px) - 16px);overflow:auto;box-sizing:border-box;padding:12px;border-radius:12px;\n background:var(--ui-surface,#1e2433);color:var(--ui-text,#e2e6ec);border:1px solid var(--ui-border,#3a4458);box-shadow:0 12px 32px rgba(0,0,0,.45);}\n #slotRight{top:calc(var(--bar-panel-top,52px) + var(--bar-center-h,0px));}\n .bar.open #slotCenter:not(:empty),.bar.open #slotRight{display:flex;}\n .bar.open #slotCenter:not(:empty){border-radius:12px 12px 0 0;border-bottom:0;padding-bottom:4px;}\n .bar.open #slotCenter:not(:empty) + #slotRight{border-radius:0 0 12px 12px;border-top:0;padding-top:4px;}\n #slotCenter > *,#slotRight > *{max-width:100%;min-width:0;box-sizing:border-box;overflow-x:auto;}\n}\n", "script": "export function init(instance){\n instance._bindingsRefresh?.();\n const resolveImg = (img) => { if(!img) return ''; if(/^(https?:|data:|blob:|\\/)/.test(img)) return img; try{ return (window.IOB&&window.IOB.imagePrefix?window.IOB.imagePrefix:'')+img; }catch(e){ return img; } };\n const cfg = (window.IOB && window.IOB.config && window.IOB.config.appBar) || {};\n const navCfg = (window.IOB && window.IOB.config && window.IOB.config.navigation) || {};\n instance.setAttribute('data-theme', navCfg.theme || 'default');\n // a phone: ⋮ opens the centre + right slots as a panel under the bar; a tap outside or Esc closes it\n (function(){\n const bar=instance._getDomElement('bar'), more=instance._getDomElement('barMore');\n if(!bar||!more) return;\n const place=()=>{ const r=bar.getBoundingClientRect(); bar.style.setProperty('--bar-panel-top',(r.bottom+6)+'px'); const c=instance._getDomElement('slotCenter'); bar.style.setProperty('--bar-center-h',(c&&c.children.length&&bar.classList.contains('open')?c.getBoundingClientRect().height:0)+'px'); };\n const close=()=>{ bar.classList.remove('open'); more.setAttribute('aria-expanded','false'); };\n more.addEventListener('click',(e)=>{ e.stopPropagation(); const open=!bar.classList.contains('open'); bar.classList.toggle('open',open); more.setAttribute('aria-expanded',String(open)); place(); requestAnimationFrame(place); });\n const outside=(e)=>{ if(bar.classList.contains('open')&&!e.composedPath().includes(bar)) close(); };\n const esc=(e)=>{ if(e.key==='Escape') close(); };\n document.addEventListener('pointerdown',outside,true);\n document.addEventListener('keydown',esc);\n window.addEventListener('resize',close);\n instance.__barOff=()=>{ document.removeEventListener('pointerdown',outside,true); document.removeEventListener('keydown',esc); window.removeEventListener('resize',close); };\n })();\n const isDesigner = !!document.querySelector('iobroker-webui-app-shell');\n if(typeof isDesigner!=='undefined' && isDesigner){ try{ document.querySelectorAll('.nav-slot-delchip').forEach(n=>n.remove()); }catch(e){} }\n function htmlFromDef(def){\n let attrs='';\n if(def.defaultAttributes) for(const a in def.defaultAttributes){ const v=def.defaultAttributes[a]; attrs+=' '+a+'=\"'+String(typeof v==='object'?JSON.stringify(v):v).replace(/\"/g,'&quot;')+'\"'; }\n let style='';\n if(def.defaultStyles) for(const k in def.defaultStyles) style+=k+':'+def.defaultStyles[k]+';';\n if(def.defaultWidth) style+='width:'+def.defaultWidth+';';\n if(def.defaultHeight) style+='height:'+def.defaultHeight+';';\n return '<'+def.tag+attrs+(style?' style=\"'+style+'\"':'')+'>'+(def.defaultContent||'')+'</'+def.tag+'>';\n }\n function wireSlot(zone, cfgRoot, key){\n if(!zone) return;\n zone.innerHTML = (cfgRoot && cfgRoot.slots && cfgRoot.slots[key]) || '';\n if(!isDesigner) return;\n if(!zone.innerHTML.trim()){ zone.dataset.empty='1'; zone.style.minHeight='30px'; zone.style.outline='1px dashed rgba(255,255,255,.28)'; zone.style.borderRadius='8px'; zone.style.margin='4px 8px'; }\n (function(){\n let chip=null,target=null,hideT=null;\n const saveFromDom=()=>{ if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=Array.from(zone.children).map(c=>c.outerHTML).join(''); if(window.IOB&&window.IOB.saveConfig) window.IOB.saveConfig(); };\n const hideChip=()=>{ if(chip) chip.style.display='none'; target=null; };\n const ensureChip=()=>{ if(chip) return chip; chip=document.createElement('div'); chip.className='nav-slot-delchip'; chip.textContent='✕'; chip.title='Delete'; chip.style.cssText='position:fixed;z-index:2147483647;width:18px;height:18px;line-height:18px;text-align:center;font:700 12px/18px sans-serif;background:#e74c3c;color:#fff;border-radius:50%;cursor:pointer;box-shadow:0 1px 5px rgba(0,0,0,.45);user-select:none;display:none;'; chip.addEventListener('mouseenter',()=>{ if(hideT){clearTimeout(hideT);hideT=null;} }); chip.addEventListener('mouseleave',hideChip); chip.addEventListener('click',e=>{ e.preventDefault(); e.stopPropagation(); if(target&&target.parentNode===zone){ target.remove(); saveFromDom(); } hideChip(); }); document.body.appendChild(chip); return chip; };\n const showChip=(el)=>{ if(hideT){clearTimeout(hideT);hideT=null;} target=el; const c=ensureChip(); const r=el.getBoundingClientRect(); c.style.left=(r.right-9)+'px'; c.style.top=(r.top-9)+'px'; c.style.display='block'; };\n zone.addEventListener('mouseover', e=>{ let el=e.target; while(el && el!==zone && el.parentNode!==zone) el=el.parentNode; if(el && el.parentNode===zone && el.nodeType===1) showChip(el); });\n zone.addEventListener('mouseleave', ()=>{ hideT=setTimeout(()=>{ if(chip && chip.style.display!=='none' && !chip.matches(':hover')) hideChip(); }, 160); });\n })();\n zone.addEventListener('dragover', e=>{ if(e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf('text/json/elementdefintion')>=0){ e.preventDefault(); zone.style.background='rgba(255,255,255,.18)'; } });\n zone.addEventListener('dragleave', ()=>{ zone.style.background=''; });\n zone.addEventListener('drop', e=>{ e.preventDefault(); e.stopPropagation(); zone.style.background='';\n try{ const td=e.dataTransfer.getData('text/json/elementdefintion'); if(!td) return; const def=JSON.parse(td); const html=htmlFromDef(def);\n if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=(cfgRoot.slots[key]||'')+html;\n if(window.IOB && window.IOB.saveConfig) window.IOB.saveConfig();\n }catch(err){ console.warn('[nav] slot drop failed', err); }\n });\n }\n const upd = () => {\n const bar=instance._getDomElement('bar'), ic=instance._getDomElement('barIcon'), cap=instance._getDomElement('barCap');\n const bg = instance.bgColor || cfg.bgColor;\n if(bg) instance.style.setProperty('--bar-bg', bg);\n const icon = instance.icon || cfg.icon;\n if(ic){ if(icon){ ic.src=resolveImg(icon); ic.style.display=''; } else ic.style.display='none'; }\n if(cap) cap.textContent = instance.caption || cfg.caption || '';\n };\n instance._assignEvent('caption-changed', upd);\n instance._assignEvent('icon-changed', upd);\n instance._assignEvent('bg-color-changed', upd);\n upd();\n wireSlot(instance._getDomElement('slotLeft'), cfg, 'left');\n wireSlot(instance._getDomElement('slotCenter'), cfg, 'center');\n wireSlot(instance._getDomElement('slotRight'), cfg, 'right');\n}\nexport function disconnectedCallback(instance){ try{ instance.__barOff&&instance.__barOff(); }catch(e){} }\n"}
1
+ {"properties":{"caption":{"type":"string","default":""},"icon":{"type":"string","default":""},"bgColor":{"type":"color","default":""}},"settings":{"width":"100%","height":"48px"},"html":"<div id=\"bar\" class=\"bar\"><div id=\"slotLeft\" class=\"bar-slot\"></div><img id=\"barIcon\" class=\"bar-icon\" alt=\"\" style=\"display:none;\"/><span id=\"barCap\" class=\"bar-cap\"></span><div id=\"slotCenter\" class=\"bar-slot bar-grow\" style=\"justify-content:center;\"></div><div id=\"slotRight\" class=\"bar-slot\"></div><button id=\"barMore\" class=\"bar-more\" type=\"button\" aria-label=\"More\" aria-expanded=\"false\">⋮</button></div>","style":":host{display:block;width:100%;height:100%;}\n.bar{display:flex;align-items:center;gap:12px;height:100%;padding:var(--bar-pad,0 16px 0 60px);color:var(--bar-fg,#fff);box-sizing:border-box;background:var(--bar-bg,linear-gradient(90deg,#1e3c72 0%,#2a5298 100%));box-shadow:0 2px 12px rgba(0,0,0,.25);-webkit-backdrop-filter:var(--ui-backdrop,none);backdrop-filter:var(--ui-backdrop,none);}\n.bar-icon{height:30px;width:auto;filter:drop-shadow(0 1px 2px rgba(0,0,0,.3));}\n.bar-cap{font-size:17px;font-weight:600;letter-spacing:.3px;}\n.bar-slot{display:flex;align-items:center;gap:8px;}\n.bar-grow{flex:1;min-width:0;}\n:host([data-theme=\"dark\"]){--bar-bg:linear-gradient(90deg,#0a0e27 0%,#1a1f3a 100%);}\n:host([data-theme=\"light\"]){--bar-bg:linear-gradient(90deg,#ffffff 0%,#eceff1 100%);}\n:host([data-theme=\"purple\"]){--bar-bg:linear-gradient(90deg,#4a1d6e 0%,#7b2d8e 100%);}\n:host([data-theme=\"green\"]){--bar-bg:linear-gradient(90deg,#1b5e20 0%,#2e7d32 100%);}\n:host([data-theme=\"orange\"]){--bar-bg:linear-gradient(90deg,#bf360c 0%,#e64a19 100%);}\n:host([data-theme=\"teal\"]){--bar-bg:linear-gradient(90deg,#004d40 0%,#00695c 100%);}\n:host([data-theme=\"auto\"]){--bar-bg:var(--ui-surface,#252b38);--bar-fg:var(--ui-text,#e2e6ec);}\n/* controls in the bar (user badge, logout, theme toggle …) take the bar's colours, not the page theme's:\n a dark bar gets light text on translucent surfaces, a light bar dark text. \"auto\" follows the page theme. */\n:host(:not([data-theme=\"auto\"])) .bar{--ui-text:var(--bar-fg,#fff);--ui-dim:rgba(255,255,255,.72);--ui-border:rgba(255,255,255,.3);\n --ui-surface:rgba(255,255,255,.08);--ui-surface-2:rgba(255,255,255,.16);--ui-input:#2a3142;--ui-page:rgba(0,0,0,.28);}\n:host([data-theme=\"light\"]){--bar-fg:#1d2733;}\n:host([data-theme=\"light\"]) .bar{--ui-dim:#5a6675;--ui-border:#c9d0db;--ui-surface:#f3f5f8;--ui-surface-2:#ffffff;--ui-input:#ffffff;--ui-page:#eef1f6;}\n.bar-cap{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.bar-more{display:none;flex:none;margin-left:auto;width:40px;height:40px;border:0;border-radius:10px;background:transparent;color:inherit;font:700 22px/1 sans-serif;cursor:pointer;}\n.bar-more:hover,.bar-more[aria-expanded=\"true\"]{background:rgba(255,255,255,.14);}\n.bar-more:focus-visible{outline:2px solid currentColor;outline-offset:2px;}\n/* a phone: the centre and right slots move into a panel the ⋮ opens */\n@media (max-width:768px){\n .bar{gap:8px;}\n .bar-cap{font-size:15px;}\n .bar-more{display:block;}\n #slotCenter,#slotRight{display:none;position:fixed;right:8px;top:var(--bar-panel-top,52px);z-index:100010;flex-direction:column;align-items:stretch;gap:10px;\n width:min(320px,calc(100vw - 16px));max-height:calc(100vh - var(--bar-panel-top,52px) - 16px);overflow:auto;box-sizing:border-box;padding:12px;border-radius:12px;\n background:var(--ui-surface,#1e2433);color:var(--ui-text,#e2e6ec);border:1px solid var(--ui-border,#3a4458);box-shadow:0 12px 32px rgba(0,0,0,.45);}\n #slotRight{top:calc(var(--bar-panel-top,52px) + var(--bar-center-h,0px));}\n .bar.open #slotCenter:not(:empty),.bar.open #slotRight{display:flex;}\n .bar.open #slotCenter:not(:empty){border-radius:12px 12px 0 0;border-bottom:0;padding-bottom:4px;}\n .bar.open #slotCenter:not(:empty) + #slotRight{border-radius:0 0 12px 12px;border-top:0;padding-top:4px;}\n #slotCenter > *,#slotRight > *{max-width:100%;min-width:0;box-sizing:border-box;overflow-x:auto;}\n}\n","script":"export function init(instance){\n instance._bindingsRefresh?.();\n const resolveImg = (img) => { if(!img) return ''; if(/^(https?:|data:|blob:|\\/)/.test(img)) return img; try{ return (window.IOB&&window.IOB.imagePrefix?window.IOB.imagePrefix:'')+img; }catch(e){ return img; } };\n const cfg = (window.IOB && window.IOB.config && window.IOB.config.appBar) || {};\n const navCfg = (window.IOB && window.IOB.config && window.IOB.config.navigation) || {};\n instance.setAttribute('data-theme', navCfg.theme || 'default');\n // a phone: ⋮ opens the centre + right slots as a panel under the bar; a tap outside or Esc closes it\n (function(){\n const bar=instance._getDomElement('bar'), more=instance._getDomElement('barMore');\n if(!bar||!more) return;\n const place=()=>{ const r=bar.getBoundingClientRect(); bar.style.setProperty('--bar-panel-top',(r.bottom+6)+'px'); const c=instance._getDomElement('slotCenter'); bar.style.setProperty('--bar-center-h',(c&&c.children.length&&bar.classList.contains('open')?c.getBoundingClientRect().height:0)+'px'); };\n const close=()=>{ bar.classList.remove('open'); more.setAttribute('aria-expanded','false'); };\n more.addEventListener('click',(e)=>{ e.stopPropagation(); const open=!bar.classList.contains('open'); bar.classList.toggle('open',open); more.setAttribute('aria-expanded',String(open)); place(); requestAnimationFrame(place); });\n const outside=(e)=>{ if(bar.classList.contains('open')&&!e.composedPath().includes(bar)) close(); };\n const esc=(e)=>{ if(e.key==='Escape') close(); };\n document.addEventListener('pointerdown',outside,true);\n document.addEventListener('keydown',esc);\n window.addEventListener('resize',close);\n instance.__barOff=()=>{ document.removeEventListener('pointerdown',outside,true); document.removeEventListener('keydown',esc); window.removeEventListener('resize',close); };\n })();\n const isDesigner = !!document.querySelector('iobroker-webui-app-shell');\n if(typeof isDesigner!=='undefined' && isDesigner){ try{ document.querySelectorAll('.nav-slot-delchip').forEach(n=>n.remove()); }catch(e){} }\n function htmlFromDef(def){\n let attrs='';\n if(def.defaultAttributes) for(const a in def.defaultAttributes){ const v=def.defaultAttributes[a]; attrs+=' '+a+'=\"'+String(typeof v==='object'?JSON.stringify(v):v).replace(/\"/g,'&quot;')+'\"'; }\n let style='';\n if(def.defaultStyles) for(const k in def.defaultStyles) style+=k+':'+def.defaultStyles[k]+';';\n if(def.defaultWidth) style+='width:'+def.defaultWidth+';';\n if(def.defaultHeight) style+='height:'+def.defaultHeight+';';\n return '<'+def.tag+attrs+(style?' style=\"'+style+'\"':'')+'>'+(def.defaultContent||'')+'</'+def.tag+'>';\n }\n function wireSlot(zone, cfgRoot, key){\n if(!zone) return;\n zone.innerHTML = (cfgRoot && cfgRoot.slots && cfgRoot.slots[key]) || '';\n if(!isDesigner) return;\n if(!zone.innerHTML.trim()){ zone.dataset.empty='1'; zone.style.minHeight='30px'; zone.style.outline='1px dashed rgba(255,255,255,.28)'; zone.style.borderRadius='8px'; zone.style.margin='4px 8px'; }\n (function(){\n let chip=null,target=null,hideT=null;\n const saveFromDom=()=>{ if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=Array.from(zone.children).map(c=>c.outerHTML).join(''); if(window.IOB&&window.IOB.saveConfig) window.IOB.saveConfig(); };\n const hideChip=()=>{ if(chip) chip.style.display='none'; target=null; };\n const ensureChip=()=>{ if(chip) return chip; chip=document.createElement('div'); chip.className='nav-slot-delchip'; chip.textContent='✕'; chip.title='Delete'; chip.style.cssText='position:fixed;z-index:2147483647;width:18px;height:18px;line-height:18px;text-align:center;font:700 12px/18px sans-serif;background:#e74c3c;color:#fff;border-radius:50%;cursor:pointer;box-shadow:0 1px 5px rgba(0,0,0,.45);user-select:none;display:none;'; chip.addEventListener('mouseenter',()=>{ if(hideT){clearTimeout(hideT);hideT=null;} }); chip.addEventListener('mouseleave',hideChip); chip.addEventListener('click',e=>{ e.preventDefault(); e.stopPropagation(); if(target&&target.parentNode===zone){ target.remove(); saveFromDom(); } hideChip(); }); document.body.appendChild(chip); return chip; };\n const showChip=(el)=>{ if(hideT){clearTimeout(hideT);hideT=null;} target=el; const c=ensureChip(); const r=el.getBoundingClientRect(); c.style.left=(r.right-9)+'px'; c.style.top=(r.top-9)+'px'; c.style.display='block'; };\n zone.addEventListener('mouseover', e=>{ let el=e.target; while(el && el!==zone && el.parentNode!==zone) el=el.parentNode; if(el && el.parentNode===zone && el.nodeType===1) showChip(el); });\n zone.addEventListener('mouseleave', ()=>{ hideT=setTimeout(()=>{ if(chip && chip.style.display!=='none' && !chip.matches(':hover')) hideChip(); }, 160); });\n })();\n zone.addEventListener('dragover', e=>{ if(e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf('text/json/elementdefintion')>=0){ e.preventDefault(); zone.style.background='rgba(255,255,255,.18)'; } });\n zone.addEventListener('dragleave', ()=>{ zone.style.background=''; });\n zone.addEventListener('drop', e=>{ e.preventDefault(); e.stopPropagation(); zone.style.background='';\n try{ const td=e.dataTransfer.getData('text/json/elementdefintion'); if(!td) return; const def=JSON.parse(td); const html=htmlFromDef(def);\n if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=(cfgRoot.slots[key]||'')+html;\n if(window.IOB && window.IOB.saveConfig) window.IOB.saveConfig();\n }catch(err){ console.warn('[nav] slot drop failed', err); }\n });\n }\n const upd = () => {\n const bar=instance._getDomElement('bar'), ic=instance._getDomElement('barIcon'), cap=instance._getDomElement('barCap');\n const bg = instance.bgColor || cfg.bgColor;\n if(bg) instance.style.setProperty('--bar-bg', bg);\n const icon = instance.icon || cfg.icon;\n if(ic){ if(icon){ ic.src=resolveImg(icon); ic.style.display=''; } else ic.style.display='none'; }\n if(cap) cap.textContent = instance.caption || cfg.caption || '';\n };\n instance._assignEvent('caption-changed', upd);\n instance._assignEvent('icon-changed', upd);\n instance._assignEvent('bg-color-changed', upd);\n upd();\n wireSlot(instance._getDomElement('slotLeft'), cfg, 'left');\n wireSlot(instance._getDomElement('slotCenter'), cfg, 'center');\n wireSlot(instance._getDomElement('slotRight'), cfg, 'right');\n}\nexport function disconnectedCallback(instance){ try{ instance.__barOff&&instance.__barOff(); }catch(e){} }\n"}
@@ -1 +1 @@
1
- {"properties":{},"settings":{"width":"220px","height":"100%"},"html":"<div id=\"sb\" class=\"sb\"><div id=\"sbTop\" class=\"sb-top\"><div id=\"sbHdr\" class=\"sb-hdr\"></div><button id=\"sbToggle\" class=\"sb-tog\" type=\"button\"></button></div><div id=\"slotTop\" class=\"sb-slot\"></div><div id=\"sbItems\" class=\"sb-items\"></div><div id=\"slotBottom\" class=\"sb-slot\"></div></div>","style":":host{display:block;width:100%;height:100%;box-sizing:border-box;}\r\n.sb{display:flex;flex-direction:column;height:100%;width:100%;background:var(--sb-bg,linear-gradient(180deg,#1e3c72 0%,#2a5298 100%));color:var(--sb-fg,rgba(255,255,255,.9));box-shadow:2px 0 18px rgba(0,0,0,.28);-webkit-backdrop-filter:var(--ui-backdrop,none);backdrop-filter:var(--ui-backdrop,none);box-sizing:border-box;}\r\n.sb-hdr{padding:18px 16px 14px;font-weight:700;font-size:16px;letter-spacing:.5px;color:var(--sb-hdr-fg,#fff);border-bottom:1px solid rgba(255,255,255,.12);white-space:nowrap;overflow:hidden;}\r\n.sb-items{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0;}\r\n.sb-items::-webkit-scrollbar{width:6px;}\r\n.sb-items::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px;}\r\n.sb-slot{display:block;}\r\n:host([data-theme=\"dark\"]){--sb-bg:linear-gradient(180deg,#0a0e27 0%,#1a1f3a 100%);--sb-fg:rgba(255,255,255,.88);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.04);--mi-hover:rgba(255,255,255,.1);--mi-active:linear-gradient(135deg,rgba(74,158,255,.35),rgba(74,158,255,.14));--mi-active-fg:#fff;--mi-active-icon:#4a9eff;}\r\n:host([data-theme=\"light\"]){--sb-bg:linear-gradient(180deg,#f5f7fa 0%,#e4e8f0 100%);--sb-fg:#37474f;--sb-hdr-fg:#1a237e;--mi-bg:rgba(0,0,0,.04);--mi-hover:rgba(33,150,243,.1);--mi-active:linear-gradient(135deg,rgba(33,150,243,.22),rgba(33,150,243,.1));--mi-active-fg:#1565c0;--mi-active-icon:#1976d2;}\r\n:host([data-theme=\"purple\"]){--sb-bg:linear-gradient(180deg,#4a1d6e 0%,#7b2d8e 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(224,64,251,.35),rgba(224,64,251,.15));--mi-active-fg:#fff;--mi-active-icon:#8e24aa;}\r\n:host([data-theme=\"green\"]){--sb-bg:linear-gradient(180deg,#1b5e20 0%,#2e7d32 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(105,240,174,.3),rgba(105,240,174,.12));--mi-active-fg:#fff;--mi-active-icon:#2e7d32;}\r\n:host([data-theme=\"orange\"]){--sb-bg:linear-gradient(180deg,#bf360c 0%,#e64a19 100%);--sb-fg:rgba(255,255,255,.92);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.08);--mi-hover:rgba(255,255,255,.18);--mi-active:linear-gradient(135deg,rgba(255,171,64,.32),rgba(255,171,64,.14));--mi-active-fg:#fff;--mi-active-icon:#e64a19;}\r\n:host([data-theme=\"teal\"]){--sb-bg:linear-gradient(180deg,#004d40 0%,#00695c 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(29,233,182,.3),rgba(29,233,182,.12));--mi-active-fg:#fff;--mi-active-icon:#00897b;}\r\n:host([data-theme=\"auto\"]){--sb-bg:var(--ui-surface,#252b38);--sb-fg:var(--ui-text,#e2e6ec);--sb-hdr-fg:var(--ui-text,#fff);--mi-bg:color-mix(in srgb,var(--ui-text,#fff) 6%,transparent);--mi-fg:var(--ui-text,#e2e6ec);--mi-hover:color-mix(in srgb,var(--ui-text,#fff) 13%,transparent);--mi-active:color-mix(in srgb,var(--accent,#2680eb) 26%,transparent);--mi-active-border:color-mix(in srgb,var(--accent,#2680eb) 55%,transparent);--mi-active-fg:var(--ui-text,#fff);--mi-active-icon:var(--accent,#2680eb);}\r\n:host([data-orient=\"h\"]) .sb{flex-direction:row;align-items:center;box-shadow:0 2px 18px rgba(0,0,0,.28);}\r\n:host([data-orient=\"h\"]) .sb-hdr{border-bottom:0;border-right:1px solid rgba(255,255,255,.12);padding:0 16px;align-self:stretch;display:flex;align-items:center;}\r\n:host([data-orient=\"h\"]) .sb-items{display:flex;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;padding:0 6px;height:100%;}\r\n:host([data-orient=\"h\"]) .sb-items::-webkit-scrollbar{height:4px;width:auto;}\r\n:host([data-orient=\"h\"]) .sb-slot{display:flex;align-items:center;gap:6px;}\r\n\n.sb-top{display:flex;align-items:center;gap:8px;min-height:52px;padding:0 10px 0 0;border-bottom:1px solid rgba(255,255,255,.12);box-sizing:border-box;flex:none;}\n.sb-top .sb-hdr{flex:1;min-width:0;border-bottom:0;text-overflow:ellipsis;}\n.sb-tog{flex:none;margin-left:auto;width:38px;height:38px;border:0;border-radius:9px;background:transparent;color:var(--sb-hdr-fg,#fff);cursor:pointer;display:grid;place-items:center;transition:background .15s;}\n.sb-tog:hover{background:var(--mi-hover,rgba(255,255,255,.1));}\n.sb-tog:focus-visible{outline:2px solid currentColor;outline-offset:2px;}\n.sb-tog svg{width:22px;height:22px;display:block;}\n:host([data-pos=\"right\"]) .sb-tog svg{transform:scaleX(-1);}\n:host([data-state=\"icons\"]) .sb-top{padding:0;justify-content:center;}\n:host([data-state=\"icons\"]) .sb-tog{margin:0 auto;}\n:host([data-orient=\"h\"]) .sb-top{display:none;}\n","script":"export function init(instance){\r\n instance._bindingsRefresh?.();\r\n const IOB = window.IOB;\r\n const cfg = (IOB && IOB.config && IOB.config.navigation) || {};\r\n instance.setAttribute('data-theme', cfg.theme || 'default');\r\n function htmlFromDef(def){\r\n let attrs='';\r\n if(def.defaultAttributes) for(const a in def.defaultAttributes){ const v=def.defaultAttributes[a]; attrs+=' '+a+'=\"'+String(typeof v==='object'?JSON.stringify(v):v).replace(/\"/g,'&quot;')+'\"'; }\r\n let style='';\r\n if(def.defaultStyles) for(const k in def.defaultStyles) style+=k+':'+def.defaultStyles[k]+';';\r\n if(def.defaultWidth) style+='width:'+def.defaultWidth+';';\r\n if(def.defaultHeight) style+='height:'+def.defaultHeight+';';\r\n return '<'+def.tag+attrs+(style?' style=\"'+style+'\"':'')+'>'+(def.defaultContent||'')+'</'+def.tag+'>';\r\n }\r\n function wireSlot(zone, cfgRoot, key){\r\n if(!zone) return;\r\n zone.innerHTML = (cfgRoot && cfgRoot.slots && cfgRoot.slots[key]) || '';\r\n if(!isDesigner) return;\r\n if(!zone.innerHTML.trim()){ zone.dataset.empty='1'; zone.style.minHeight='30px'; zone.style.outline='1px dashed rgba(255,255,255,.28)'; zone.style.borderRadius='8px'; zone.style.margin='4px 8px'; }\r\n (function(){\r\n let chip=null,target=null,hideT=null;\r\n const saveFromDom=()=>{ if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=Array.from(zone.children).map(c=>c.outerHTML).join(''); if(window.IOB&&window.IOB.saveConfig) window.IOB.saveConfig(); };\r\n const hideChip=()=>{ if(chip) chip.style.display='none'; target=null; };\r\n const ensureChip=()=>{ if(chip) return chip; chip=document.createElement('div'); chip.className='nav-slot-delchip'; chip.textContent='✕'; chip.title='Delete'; chip.style.cssText='position:fixed;z-index:2147483647;width:18px;height:18px;line-height:18px;text-align:center;font:700 12px/18px sans-serif;background:#e74c3c;color:#fff;border-radius:50%;cursor:pointer;box-shadow:0 1px 5px rgba(0,0,0,.45);user-select:none;display:none;'; chip.addEventListener('mouseenter',()=>{ if(hideT){clearTimeout(hideT);hideT=null;} }); chip.addEventListener('mouseleave',hideChip); chip.addEventListener('click',e=>{ e.preventDefault(); e.stopPropagation(); if(target&&target.parentNode===zone){ target.remove(); saveFromDom(); } hideChip(); }); document.body.appendChild(chip); return chip; };\r\n const showChip=(el)=>{ if(hideT){clearTimeout(hideT);hideT=null;} target=el; const c=ensureChip(); const r=el.getBoundingClientRect(); c.style.left=(r.right-9)+'px'; c.style.top=(r.top-9)+'px'; c.style.display='block'; };\r\n zone.addEventListener('mouseover', e=>{ let el=e.target; while(el && el!==zone && el.parentNode!==zone) el=el.parentNode; if(el && el.parentNode===zone && el.nodeType===1) showChip(el); });\r\n zone.addEventListener('mouseleave', ()=>{ hideT=setTimeout(()=>{ if(chip && chip.style.display!=='none' && !chip.matches(':hover')) hideChip(); }, 160); });\r\n })();\r\n zone.addEventListener('dragover', e=>{ if(e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf('text/json/elementdefintion')>=0){ e.preventDefault(); zone.style.background='rgba(255,255,255,.18)'; } });\r\n zone.addEventListener('dragleave', ()=>{ zone.style.background=''; });\r\n zone.addEventListener('drop', e=>{ e.preventDefault(); e.stopPropagation(); zone.style.background='';\r\n try{ const td=e.dataTransfer.getData('text/json/elementdefintion'); if(!td) return; const def=JSON.parse(td); const html=htmlFromDef(def);\r\n if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=(cfgRoot.slots[key]||'')+html;\r\n if(window.IOB && window.IOB.saveConfig) window.IOB.saveConfig();\r\n }catch(err){ console.warn('[nav] slot drop failed', err); }\r\n });\r\n }\r\n const isDesigner = !!document.querySelector('iobroker-webui-app-shell');\r\n if(typeof isDesigner!=='undefined' && isDesigner){ try{ document.querySelectorAll('.nav-slot-delchip').forEach(n=>n.remove()); }catch(e){} }\r\n const hdr=instance._getDomElement('sbHdr'), items=instance._getDomElement('sbItems');\r\n if(cfg.bgColor) instance.style.setProperty('--sb-bg', cfg.bgColor);\r\n if(cfg.textColor){ instance.style.setProperty('--sb-fg', cfg.textColor); instance.style.setProperty('--mi-fg', cfg.textColor); }\r\n if(cfg.menuHeaderTextColor) instance.style.setProperty('--sb-hdr-fg', cfg.menuHeaderTextColor);\r\n if(cfg.bgColorSelected){ instance.style.setProperty('--mi-active', cfg.bgColorSelected); instance.style.setProperty('--mi-active-icon', cfg.bgColorSelected); }\r\n if(cfg.textColorSelected) instance.style.setProperty('--mi-active-fg', cfg.textColorSelected);\r\n if(hdr){ hdr.textContent=cfg.menuHeaderText||''; hdr.style.display=cfg.menuHeaderText?'':'none'; }\r\n // how the menu opens: full, icons only, hidden, or as this browser left it last time\r\n const stateKey='webui.nav.state.'+((IOB&&IOB.configPath)||'');\r\n const startAs=['expanded','icons','hidden','last'].includes(cfg.startState)?cfg.startState:'expanded';\r\n let state=startAs;\r\n if(startAs==='last'){ try{ state=localStorage.getItem(stateKey)||'expanded'; }catch(e){ state='expanded'; } }\r\n if(!['expanded','icons','hidden'].includes(state)) state='expanded';\r\n if(state==='hidden'&&cfg.doNotHideCompletely) state='icons'; // a phone: a side menu that starts open covers the screen — start closed, the ☰ opens it\n try{ const side=!cfg.position||cfg.position==='left'||cfg.position==='right'; if(side&&state==='expanded'&&window.matchMedia&&matchMedia('(max-width: 768px)').matches) state=cfg.doNotHideCompletely?'icons':'hidden'; }catch(e){}\r\n const ICONS_W=64; let burger=null; let hasItems=false;\r\n const navWidth = cfg.menuWidth ? (/^\\d+$/.test(String(cfg.menuWidth))?cfg.menuWidth+'px':String(cfg.menuWidth)) : '220px';\r\n // Groups: none (a flat list), 'folder' (the screen's folder: tests/pump → tests) or 'group'\r\n // (the screen's own Navigation → Group). A group opens and closes; a \"/\" nests it.\r\n const groupBy = cfg.groupBy || 'none';\r\n const groupsKey='webui.nav.groups.'+((IOB&&IOB.configPath)||'');\r\n let openGroups={}; try{ openGroups=JSON.parse(localStorage.getItem(groupsKey)||'{}')||{}; }catch(e){}\r\n const saveGroups=()=>{ try{ localStorage.setItem(groupsKey, JSON.stringify(openGroups)); }catch(e){} };\r\n const groupOf=(e)=> groupBy==='folder' ? (e.name.includes('/') ? e.name.slice(0, e.name.lastIndexOf('/')) : '') : groupBy==='group' ? String(e.group||'').replace(/^\\/+|\\/+$/g,'') : '';\r\n const setActive=(name)=>{\r\n if(!items) return;\r\n items.querySelectorAll('webui-navigation-menu-item').forEach(mi=>{ if(mi.kind!=='group') mi.selected=(mi.screenName===name); });\r\n // the group holding the open screen is opened\r\n const own=items.querySelector('webui-navigation-menu-item[data-entry=\"'+CSS.escape(String(name))+'\"]');\r\n for(let g=own&&own.parentElement; g&&g!==items; g=g.parentElement){ if(g.dataset.group!=null && g.hidden){ g.hidden=false; const head=g.previousElementSibling; if(head) head.expanded=true; } }\r\n };\r\n const navTo=(name,kind)=>{ if(IOB&&IOB.changeView) IOB.changeView.emit(kind==='report'?{name:name,type:'report'}:name); setActive(name); if(cfg.hideAfterSelection) setState('hidden'); };\r\n async function loadEntries(){\r\n if(!IOB||!IOB.getAllNames) return [];\r\n // Reports carry every screen setting, navigation among them, so they belong in the\r\n // menu exactly as screens do — the entry remembers which kind it is so the runtime opens\r\n // a .report in the report viewer rather than looking for a screen of that name.\r\n // Screens and reports are listed at the same time and every file is read at the same\r\n // time — each listing is a round trip to the server, so waiting for one before the next\r\n // is what made the menu slow.\r\n const kinds=await Promise.all(['screen','report'].map(async k=>[k,await IOB.getAllNames(k).catch(()=>[])]));\r\n const all=await Promise.all(kinds.flatMap(([kind,names])=>names.map(async n=>{ const o=await IOB.getWebuiObject(kind,n).catch(()=>null); const nv=o&&o.settings&&o.settings.navigation; return nv&&nv.show?{name:n,kind:kind,title:nv.title||n,order:parseFloat(nv.order)||0,image:nv.image||'',group:nv.group||'',badge:nv.badge||''}:null; })));\r\n const list=all.filter(Boolean);\r\n list.sort((a,b)=>a.order-b.order); return list;\r\n }\r\n // The menu this browser saw last is drawn at once; the fresh list replaces it when it differs.\r\n const menuCacheKey='webui.nav.menu.'+((IOB&&IOB.configPath)||'');\r\n async function render(){\r\n // items made before the menu item is defined lose what is set on them (level, image):\r\n // wait for the definition, at most 3 s\r\n if(window.customElements&&!customElements.get('webui-navigation-menu-item')) await Promise.race([customElements.whenDefined('webui-navigation-menu-item'), new Promise(r=>setTimeout(r,3000))]);\r\n let cached=null;\r\n if(!isDesigner){ try{ cached=JSON.parse(localStorage.getItem(menuCacheKey)||'null'); }catch(e){} if(Array.isArray(cached)) draw(cached); }\r\n const list=await loadEntries();\r\n const fresh=JSON.stringify(list);\r\n if(!Array.isArray(cached)||JSON.stringify(cached)!==fresh) draw(list);\r\n if(!isDesigner){ try{ localStorage.setItem(menuCacheKey,fresh); }catch(e){} }\r\n }\r\n function itemFor(e, level){\r\n const mi=document.createElement('webui-navigation-menu-item');\r\n mi.dataset.managed='1'; mi.dataset.entry=e.name;\r\n // inside its folder an entry without its own title shows its name without the folder\r\n mi.screenName=e.name; mi.title=(groupBy==='folder'&&e.title===e.name&&e.name.includes('/'))?e.name.slice(e.name.lastIndexOf('/')+1):e.title; mi.image=e.image; mi.level=level; mi.tint=cfg.tintIcons!==false; mi.compact=(state==='icons');\r\n mi.badge=badgeVals.has(e.name)?badgeVals.get(e.name):'';\r\n mi.addEventListener('menuitemclick',()=>navTo(e.name,e.kind));\r\n return mi;\r\n }\r\n // entries → a tree of groups; a group sits where its first entry (by order) sits\r\n function tree(list){\r\n const root={sub:new Map(),rows:[]};\r\n for(const e of list){\r\n let node=root, path='';\r\n for(const seg of groupOf(e).split('/').filter(Boolean)){\r\n path=path?path+'/'+seg:seg;\r\n if(!node.sub.has(seg)){ const g={title:seg,path:path,sub:new Map(),rows:[]}; node.sub.set(seg,g); node.rows.push({group:g,order:e.order}); }\r\n node=node.sub.get(seg);\r\n }\r\n node.rows.push({entry:e,order:e.order});\r\n }\r\n // a group with its own order (Settings → Navigation → Groups) moves there\r\n (function sortNode(n){ for(const r of n.rows) if(r.group){ const o=parseFloat(groupCfg(r.group.path).order); if(!isNaN(o)) r.order=o; } n.rows.sort((a,b)=>a.order-b.order); for(const g of n.sub.values()) sortNode(g); })(root);\r\n return root;\r\n }\r\n // Settings → Navigation → Groups: own title (a translation key works too), icon and order\r\n const groupCfg=(path)=>(cfg.groups&&cfg.groups[path])||{};\r\n const groupTitle=(g)=>{ const t=groupCfg(g.path).title||g.title; try{ const tr=IOB&&IOB.t?IOB.t(t):t; return tr&&tr!==t?tr:t; }catch(e){ return t; } };\r\n // Icons only: a group opens its entries in a panel beside the sidebar (hover or click).\r\n // The panel lives in the page (the sidebar clips what is inside it) and takes the menu's colours.\r\n let fly=null, flyT=0;\r\n const VARS=['--sb-bg','--sb-fg','--mi-bg','--mi-fg','--mi-hover','--mi-active','--mi-active-border','--mi-active-fg','--mi-active-icon'];\r\n function closeFlyout(){ clearTimeout(flyT); if(fly){ fly.remove(); fly=null; } }\r\n function closeSoon(){ clearTimeout(flyT); flyT=setTimeout(closeFlyout,280); }\r\n function openFlyout(head, g){\r\n clearTimeout(flyT);\r\n if(fly&&fly._g===g) return;\r\n closeFlyout();\r\n fly=document.createElement('div'); fly._g=g; fly.className='nav-flyout';\r\n const cs=getComputedStyle(instance); for(const v of VARS){ const x=cs.getPropertyValue(v); if(x&&x.trim()) fly.style.setProperty(v,x.trim()); }\r\n const r=head.getBoundingClientRect(), sb=instance.getBoundingClientRect(), top=Math.max(8,r.top-8);\r\n // beside a vertical menu (on its inner side), under a top bar, over a bottom bar\r\n const place=horiz?(pos==='top'?{left:Math.max(6,r.left)+'px',top:(sb.bottom+6)+'px',maxHeight:(innerHeight-sb.bottom-18)+'px'}:{left:Math.max(6,r.left)+'px',bottom:(innerHeight-sb.top+6)+'px',maxHeight:(sb.top-18)+'px'})\r\n :(pos==='right'?{right:(innerWidth-sb.left+6)+'px',top:top+'px',maxHeight:(innerHeight-top-12)+'px'}:{left:((state==='icons'?sb.left+ICONS_W:sb.right)+6)+'px',top:top+'px',maxHeight:(innerHeight-top-12)+'px'});\r\n Object.assign(fly.style,place,{position:'fixed',zIndex:'100001',minWidth:'210px',maxWidth:'320px',overflow:'auto',padding:'6px 0 8px',borderRadius:'12px',background:'var(--sb-bg,linear-gradient(180deg,#1e3c72 0%,#2a5298 100%))',color:'var(--sb-fg,rgba(255,255,255,.9))',boxShadow:'0 10px 30px rgba(0,0,0,.38)'});\r\n const h=document.createElement('div'); h.textContent=groupTitle(g); Object.assign(h.style,{padding:'8px 16px 6px',fontWeight:'700',fontSize:'13px',opacity:'.85',whiteSpace:'nowrap'}); fly.appendChild(h);\r\n const current=(document.getElementById('viewer')||{}).screenName;\r\n (function add(node, level){ for(const row of node.rows){\r\n if(row.entry){ const mi=itemFor(row.entry, level); mi.compact=false; mi.selected=(row.entry.name===current); mi.addEventListener('menuitemclick',closeFlyout); fly.appendChild(mi); }\r\n else { const sub=document.createElement('webui-navigation-menu-item'); sub.dataset.managed='1'; sub.kind='group'; sub.expanded=true; sub.title=groupTitle(row.group); sub.image=groupCfg(row.group.path).image||''; sub.level=level; sub.tint=cfg.tintIcons!==false; fly.appendChild(sub); add(row.group, level+1); }\r\n } })(g, 0);\r\n fly.addEventListener('mouseenter',()=>clearTimeout(flyT)); fly.addEventListener('mouseleave',closeSoon);\r\n document.body.appendChild(fly);\r\n }\r\n if(!isDesigner){\r\n // the runtime rebuilds the sidebar when the settings change: a removed sidebar lets go\r\n const onDown=e=>{ if(!instance.isConnected){ document.removeEventListener('pointerdown',onDown,true); closeFlyout(); return; } if(fly&&!e.composedPath().some(n=>n===fly||n===instance)) closeFlyout(); };\r\n const onKey=e=>{ if(!instance.isConnected){ document.removeEventListener('keydown',onKey); return; } if(e.key==='Escape') closeFlyout(); };\r\n document.addEventListener('pointerdown',onDown,true);\r\n document.addEventListener('keydown',onKey);\r\n }\r\n function drawNode(node, parent, level){\r\n for(const r of node.rows){\r\n if(r.entry){ parent.appendChild(itemFor(r.entry, level)); continue; }\r\n const g=r.group, head=document.createElement('webui-navigation-menu-item'), box=document.createElement('div');\r\n head.dataset.managed='1'; head.kind='group'; head.title=groupTitle(g); head.image=groupCfg(g.path).image||''; head.level=level; head.tint=cfg.tintIcons!==false; head.compact=(state==='icons');\r\n head.expanded=!!openGroups[g.path];\r\n box.dataset.group=g.path; box.hidden=!openGroups[g.path];\r\n head.addEventListener('menugroupclick',()=>{ if(state==='icons'||horiz){ fly&&fly._g===g?closeFlyout():openFlyout(head,g); return; } box.hidden=!box.hidden; head.expanded=!box.hidden; openGroups[g.path]=!box.hidden; saveGroups(); });\r\n head.addEventListener('mouseenter',()=>{ if(state==='icons'||horiz) openFlyout(head,g); });\r\n head.addEventListener('mouseleave',()=>{ if(state==='icons'||horiz) closeSoon(); });\r\n head._entries=(function all(n){ return n.rows.flatMap(x=>x.entry?[x.entry.name]:all(x.group)); })(g);\r\n parent.appendChild(head); parent.appendChild(box);\r\n drawNode(g, box, level+1);\r\n }\r\n }\r\n // Badge: a screen's Navigation → Badge names a state; its value shows on the entry (a number,\r\n // or a dot for true) and adds up on the group that holds it — an alarm count, for example.\r\n const badgeVals=new Map(); let badgeSubs=[];\r\n const badgeOf=(v)=>{ if(v===true||v==='true') return true; const n=Number(v); if(v!==''&&v!=null&&!isNaN(n)) return n>0?n:''; return (v==null||v===false||v==='false')?'':String(v); };\r\n function showBadge(name){\r\n const v=badgeVals.has(name)?badgeVals.get(name):'';\r\n const sel='webui-navigation-menu-item[data-entry=\"'+CSS.escape(String(name))+'\"]';\r\n for(const root of [items,fly]) if(root) root.querySelectorAll(sel).forEach(mi=>{ mi.badge=v; });\r\n // groups: the sum of their numbers, or a dot when only dots\r\n const heads=[]; for(const root of [items,fly]) if(root) root.querySelectorAll('webui-navigation-menu-item').forEach(h=>{ if(h.kind==='group'&&h._entries) heads.push(h); });\r\n for(const h of heads){ let sum=0, dot=false; for(const n of h._entries){ const x=badgeVals.get(n); if(typeof x==='number') sum+=x; else if(x) dot=true; } h.badge=sum>0?sum:(dot?true:''); }\r\n }\r\n function watchBadges(list){\r\n if(isDesigner||!IOB||!IOB.subscribeState) return;\r\n for(const [id,cb] of badgeSubs) try{ IOB.unsubscribeState(id,cb); }catch(e){}\r\n badgeSubs=[];\r\n for(const e of list){\r\n if(!e.badge) continue;\r\n const cb=(_id,st)=>{ if(!instance.isConnected){ for(const [i,c] of badgeSubs) try{ IOB.unsubscribeState(i,c); }catch(x){} badgeSubs=[]; closeFlyout(); return; } badgeVals.set(e.name,badgeOf(st?st.val:'')); showBadge(e.name); };\r\n IOB.subscribeState(e.badge,cb); badgeSubs.push([e.badge,cb]);\r\n Promise.resolve(IOB.getState(e.badge)).then(st=>cb(e.badge,st)).catch(()=>{});\r\n }\r\n }\r\n function draw(list){\r\n hasItems=list.length>0;\r\n if(items){ items.innerHTML='';\r\n if(groupBy==='none') for(const e of list) items.appendChild(itemFor(e, 0));\r\n else drawNode(tree(list), items, 0);\r\n const sv=document.getElementById('viewer'); if(sv&&sv.screenName) setActive(sv.screenName);\r\n }\r\n watchBadges(list);\r\n if(!isDesigner){ if(hasItems && !cfg.hideMenu) applyRuntimeLayout(); else { const v=document.getElementById('viewer'); if(v){ v.style.left='0'; v.style.width='100%'; } instance.style.display='none'; } }\r\n }\r\n // Position (left | right | top | bottom) and layout (push: the screen makes room; overlay: the\r\n // menu opens over the screen, which keeps its size). An older \"horizontal\" orientation\r\n // without a position means top. Top and bottom are a bar: groups open in a panel.\r\n const pos=['left','right','top','bottom'].includes(cfg.position)?cfg.position:(cfg.orientation==='horizontal'?'top':'left');\r\n const horiz=(pos==='top'||pos==='bottom');\r\n const overlay=(cfg.layout==='overlay');\r\n const BAR_H=56, BAR_ICONS_H=44;\r\n if(horiz) instance.setAttribute('data-orient','h'); else instance.removeAttribute('data-orient');\r\n let barTop=0;\r\n function applyRuntimeLayout(){\r\n const ab=document.getElementById('__nav-appbar-host'); barTop=ab?ab.offsetHeight:0;\r\n const st=instance.style; st.position='fixed'; st.zIndex='99999'; st.overflow='hidden';\r\n st.left=st.right=st.top=st.bottom='';\r\n if(pos==='left'){ st.left='0'; st.top=barTop+'px'; st.bottom='0'; }\r\n else if(pos==='right'){ st.right='0'; st.top=barTop+'px'; st.bottom='0'; }\r\n else if(pos==='top'){ st.left='0'; st.right='0'; st.top=barTop+'px'; }\r\n else { st.left='0'; st.right='0'; st.bottom='0'; }\r\n if(!burger){\r\n burger=document.createElement('div'); burger.id='__nav-burger'; burger.textContent='☰'; const inBar=barTop>0;\r\n // the corner on the menu's side: in the app bar (a right menu: its right corner), or\r\n // without an app bar over the page\r\n const corner=inBar?(pos==='right'?'top:0;right:0;':'top:0;left:0;'):(pos==='right'?'top:8px;right:8px;':pos==='bottom'?'bottom:6px;left:8px;':'top:'+(pos==='top'?7:8)+'px;left:8px;');\r\n burger.style.cssText='position:fixed;'+corner+'z-index:100002;width:'+(inBar?barTop:42)+'px;height:'+(inBar?barTop:42)+'px;display:flex;align-items:center;justify-content:center;font-size:22px;cursor:pointer;'+(inBar?'':'border-radius:8px;')+'background:'+(cfg.showButtonBg||(inBar?'transparent':'rgba(0,0,0,.45)'))+';color:'+(cfg.showButtonColor||'#fff')+';transition:background .15s;';\r\n burger.onclick=()=>{ if(horiz) cycle(); else setState('expanded'); }; document.body.appendChild(burger);\r\n }\r\n // the app bar keeps room for the ☰ on the side it sits on\r\n padBar();\r\n // a bar without an app bar: the floating ☰ needs room at its start\r\n st.paddingLeft=(horiz&&!barTop)?'56px':'';\r\n setState(state);\r\n }\r\n // The sidebar and the screen move together, with the same easing: the screen's size\r\n // changes every frame, so a stretched screen scales smoothly instead of jumping. The first\r\n // placement is not animated.\r\n const EASE='.32s cubic-bezier(.22,.8,.24,1)';\r\n const still=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;\r\n let placed=false;\r\n function setState(s){\r\n closeFlyout();\r\n state=s;\r\n if(startAs==='last'&&!isDesigner){ try{ localStorage.setItem(stateKey,s); }catch(e){} } const v=document.getElementById('viewer');\r\n const anim=placed&&!still; placed=true;\r\n instance.style.transition=anim?(horiz?'height ':'width ')+EASE+',transform '+EASE:'none';\r\n if(v) v.style.transition=anim?['left','top','width','height'].map(p=>p+' '+EASE).join(','):'none';\r\n instance.setAttribute('data-state', state); paintToggle(); padBar();\r\n const compact=(state==='icons');\r\n if(items) items.querySelectorAll('webui-navigation-menu-item').forEach(mi=>{ mi.compact=compact; mi.toggleAttribute('data-h',horiz); });\r\n // icons only, and any bar: a group's entries open in the panel, not under it\r\n if(items) items.querySelectorAll('div[data-group]').forEach(box=>{ box.style.display=(compact||horiz)?'none':''; });\r\n const hidden=(state==='hidden');\r\n let size='0px';\r\n if(state==='expanded'){ size=horiz?BAR_H+'px':navWidth; if(hdr)hdr.style.display=cfg.menuHeaderText?'':'none'; }\r\n else if(state==='icons'){ size=(horiz?BAR_ICONS_H:ICONS_W)+'px'; if(hdr)hdr.style.display='none'; }\r\n if(horiz){ instance.style.width=''; instance.style.height=size; instance.style.transform=''; }\r\n else { instance.style.height=''; if(!hidden) instance.style.width=size; instance.style.transform=hidden?(pos==='right'?'translateX(110%)':'translateX(-110%)'):'translateX(0)'; }\r\n if(v){\r\n const room=(overlay||hidden)?'0px':size;\r\n let l='0px', t=barTop+'px', w='100%', h='calc(100% - '+barTop+'px)';\r\n if(pos==='left'){ l=room; w='calc(100% - '+room+')'; }\r\n else if(pos==='right'){ w='calc(100% - '+room+')'; }\r\n else if(pos==='top'){ t='calc('+barTop+'px + '+room+')'; h='calc(100% - '+barTop+'px - '+room+')'; }\r\n else { h='calc(100% - '+barTop+'px - '+room+')'; }\r\n v.style.left=l; v.style.top=t; v.style.width=w; v.style.height=h;\r\n }\r\n }\r\n // the app bar keeps room for the ☰ on the side it sits on — a vertical menu only while it is hidden\r\n function padBar(){\r\n const ab=document.getElementById('__nav-appbar-host'), abEl=ab&&ab.querySelector('webui-navigation-app-bar');\r\n const room=(horiz||state==='hidden')?(barTop+12)+'px':'16px';\r\n if(abEl) abEl.style.setProperty('--bar-pad', pos==='right'?'0 '+room+' 0 16px':'0 16px 0 '+room);\r\n if(burger) burger.style.display=(horiz||state==='hidden')?'flex':'none';\r\n }\r\n // the toggle in the menu's header (as in most UIs): full ⇄ icons only; on a phone full ⇄ hidden\r\n const TOG_CLOSE='<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6h11M4 12h8M4 18h11M20 8l-4 4 4 4\"/></svg>';\r\n const TOG_OPEN='<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6h11M4 12h8M4 18h11M16 8l4 4-4 4\"/></svg>';\r\n const tog=instance._getDomElement('sbToggle');\r\n const paintToggle=()=>{ if(!tog) return; const open=(state==='expanded'); tog.innerHTML=open?TOG_CLOSE:TOG_OPEN; tog.title=open?'Collapse menu':'Expand menu'; tog.setAttribute('aria-label',tog.title); tog.setAttribute('aria-expanded',String(open)); };\r\n if(tog) tog.addEventListener('click',()=>{\r\n if(state!=='expanded'){ setState('expanded'); return; }\r\n const phone=window.matchMedia&&matchMedia('(max-width: 768px)').matches;\r\n setState(phone&&!cfg.doNotHideCompletely?'hidden':'icons');\r\n });\r\n if(pos==='right') instance.setAttribute('data-pos','right'); else instance.removeAttribute('data-pos');\r\n function cycle(){ const o=['expanded','icons','hidden']; let n=o[(o.indexOf(state)+1)%o.length]; if(n==='hidden'&&cfg.doNotHideCompletely)n='icons'; setState(n); }\r\n instance.setAttribute('data-state', state); paintToggle();\r\n wireSlot(instance._getDomElement('slotTop'), cfg, 'top');\r\n wireSlot(instance._getDomElement('slotBottom'), cfg, 'bottom');\r\n render();\r\n if(IOB&&IOB.changeView) IOB.changeView.on(v=>setActive(v));\r\n const sv=document.getElementById('viewer'); if(sv&&sv.screenName) setActive(sv.screenName);\r\n}"}
1
+ {"properties":{},"settings":{"width":"220px","height":"100%"},"html":"<div id=\"sb\" class=\"sb\"><div id=\"sbHdr\" class=\"sb-hdr\"></div><div id=\"slotTop\" class=\"sb-slot\"></div><div id=\"sbItems\" class=\"sb-items\"></div><div id=\"slotBottom\" class=\"sb-slot\"></div></div>","style":":host{display:block;width:100%;height:100%;box-sizing:border-box;}\r\n.sb{display:flex;flex-direction:column;height:100%;width:100%;background:var(--sb-bg,linear-gradient(180deg,#1e3c72 0%,#2a5298 100%));color:var(--sb-fg,rgba(255,255,255,.9));box-shadow:2px 0 18px rgba(0,0,0,.28);-webkit-backdrop-filter:var(--ui-backdrop,none);backdrop-filter:var(--ui-backdrop,none);box-sizing:border-box;}\r\n.sb-hdr{padding:18px 16px 14px;font-weight:700;font-size:16px;letter-spacing:.5px;color:var(--sb-hdr-fg,#fff);border-bottom:1px solid rgba(255,255,255,.12);white-space:nowrap;overflow:hidden;}\r\n.sb-items{flex:1;overflow-y:auto;overflow-x:hidden;padding:8px 0;}\r\n.sb-items::-webkit-scrollbar{width:6px;}\r\n.sb-items::-webkit-scrollbar-thumb{background:rgba(255,255,255,.3);border-radius:3px;}\r\n.sb-slot{display:block;}\r\n:host([data-theme=\"dark\"]){--sb-bg:linear-gradient(180deg,#0a0e27 0%,#1a1f3a 100%);--sb-fg:rgba(255,255,255,.88);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.04);--mi-hover:rgba(255,255,255,.1);--mi-active:linear-gradient(135deg,rgba(74,158,255,.35),rgba(74,158,255,.14));--mi-active-fg:#fff;--mi-active-icon:#4a9eff;}\r\n:host([data-theme=\"light\"]){--sb-bg:linear-gradient(180deg,#f5f7fa 0%,#e4e8f0 100%);--sb-fg:#37474f;--sb-hdr-fg:#1a237e;--mi-bg:rgba(0,0,0,.04);--mi-hover:rgba(33,150,243,.1);--mi-active:linear-gradient(135deg,rgba(33,150,243,.22),rgba(33,150,243,.1));--mi-active-fg:#1565c0;--mi-active-icon:#1976d2;}\r\n:host([data-theme=\"purple\"]){--sb-bg:linear-gradient(180deg,#4a1d6e 0%,#7b2d8e 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(224,64,251,.35),rgba(224,64,251,.15));--mi-active-fg:#fff;--mi-active-icon:#8e24aa;}\r\n:host([data-theme=\"green\"]){--sb-bg:linear-gradient(180deg,#1b5e20 0%,#2e7d32 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(105,240,174,.3),rgba(105,240,174,.12));--mi-active-fg:#fff;--mi-active-icon:#2e7d32;}\r\n:host([data-theme=\"orange\"]){--sb-bg:linear-gradient(180deg,#bf360c 0%,#e64a19 100%);--sb-fg:rgba(255,255,255,.92);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.08);--mi-hover:rgba(255,255,255,.18);--mi-active:linear-gradient(135deg,rgba(255,171,64,.32),rgba(255,171,64,.14));--mi-active-fg:#fff;--mi-active-icon:#e64a19;}\r\n:host([data-theme=\"teal\"]){--sb-bg:linear-gradient(180deg,#004d40 0%,#00695c 100%);--sb-fg:rgba(255,255,255,.9);--sb-hdr-fg:#fff;--mi-bg:rgba(255,255,255,.07);--mi-hover:rgba(255,255,255,.16);--mi-active:linear-gradient(135deg,rgba(29,233,182,.3),rgba(29,233,182,.12));--mi-active-fg:#fff;--mi-active-icon:#00897b;}\r\n:host([data-theme=\"auto\"]){--sb-bg:var(--ui-surface,#252b38);--sb-fg:var(--ui-text,#e2e6ec);--sb-hdr-fg:var(--ui-text,#fff);--mi-bg:color-mix(in srgb,var(--ui-text,#fff) 6%,transparent);--mi-fg:var(--ui-text,#e2e6ec);--mi-hover:color-mix(in srgb,var(--ui-text,#fff) 13%,transparent);--mi-active:color-mix(in srgb,var(--accent,#2680eb) 26%,transparent);--mi-active-border:color-mix(in srgb,var(--accent,#2680eb) 55%,transparent);--mi-active-fg:var(--ui-text,#fff);--mi-active-icon:var(--accent,#2680eb);}\r\n:host([data-orient=\"h\"]) .sb{flex-direction:row;align-items:center;box-shadow:0 2px 18px rgba(0,0,0,.28);}\r\n:host([data-orient=\"h\"]) .sb-hdr{border-bottom:0;border-right:1px solid rgba(255,255,255,.12);padding:0 16px;align-self:stretch;display:flex;align-items:center;}\r\n:host([data-orient=\"h\"]) .sb-items{display:flex;flex-direction:row;align-items:center;overflow-x:auto;overflow-y:hidden;padding:0 6px;height:100%;}\r\n:host([data-orient=\"h\"]) .sb-items::-webkit-scrollbar{height:4px;width:auto;}\r\n:host([data-orient=\"h\"]) .sb-slot{display:flex;align-items:center;gap:6px;}\r\n\n","script":"export function init(instance){\r\n instance._bindingsRefresh?.();\r\n const IOB = window.IOB;\r\n const cfg = (IOB && IOB.config && IOB.config.navigation) || {};\r\n instance.setAttribute('data-theme', cfg.theme || 'default');\r\n function htmlFromDef(def){\r\n let attrs='';\r\n if(def.defaultAttributes) for(const a in def.defaultAttributes){ const v=def.defaultAttributes[a]; attrs+=' '+a+'=\"'+String(typeof v==='object'?JSON.stringify(v):v).replace(/\"/g,'&quot;')+'\"'; }\r\n let style='';\r\n if(def.defaultStyles) for(const k in def.defaultStyles) style+=k+':'+def.defaultStyles[k]+';';\r\n if(def.defaultWidth) style+='width:'+def.defaultWidth+';';\r\n if(def.defaultHeight) style+='height:'+def.defaultHeight+';';\r\n return '<'+def.tag+attrs+(style?' style=\"'+style+'\"':'')+'>'+(def.defaultContent||'')+'</'+def.tag+'>';\r\n }\r\n function wireSlot(zone, cfgRoot, key){\r\n if(!zone) return;\r\n zone.innerHTML = (cfgRoot && cfgRoot.slots && cfgRoot.slots[key]) || '';\r\n if(!isDesigner) return;\r\n if(!zone.innerHTML.trim()){ zone.dataset.empty='1'; zone.style.minHeight='30px'; zone.style.outline='1px dashed rgba(255,255,255,.28)'; zone.style.borderRadius='8px'; zone.style.margin='4px 8px'; }\r\n (function(){\r\n let chip=null,target=null,hideT=null;\r\n const saveFromDom=()=>{ if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=Array.from(zone.children).map(c=>c.outerHTML).join(''); if(window.IOB&&window.IOB.saveConfig) window.IOB.saveConfig(); };\r\n const hideChip=()=>{ if(chip) chip.style.display='none'; target=null; };\r\n const ensureChip=()=>{ if(chip) return chip; chip=document.createElement('div'); chip.className='nav-slot-delchip'; chip.textContent='✕'; chip.title='Delete'; chip.style.cssText='position:fixed;z-index:2147483647;width:18px;height:18px;line-height:18px;text-align:center;font:700 12px/18px sans-serif;background:#e74c3c;color:#fff;border-radius:50%;cursor:pointer;box-shadow:0 1px 5px rgba(0,0,0,.45);user-select:none;display:none;'; chip.addEventListener('mouseenter',()=>{ if(hideT){clearTimeout(hideT);hideT=null;} }); chip.addEventListener('mouseleave',hideChip); chip.addEventListener('click',e=>{ e.preventDefault(); e.stopPropagation(); if(target&&target.parentNode===zone){ target.remove(); saveFromDom(); } hideChip(); }); document.body.appendChild(chip); return chip; };\r\n const showChip=(el)=>{ if(hideT){clearTimeout(hideT);hideT=null;} target=el; const c=ensureChip(); const r=el.getBoundingClientRect(); c.style.left=(r.right-9)+'px'; c.style.top=(r.top-9)+'px'; c.style.display='block'; };\r\n zone.addEventListener('mouseover', e=>{ let el=e.target; while(el && el!==zone && el.parentNode!==zone) el=el.parentNode; if(el && el.parentNode===zone && el.nodeType===1) showChip(el); });\r\n zone.addEventListener('mouseleave', ()=>{ hideT=setTimeout(()=>{ if(chip && chip.style.display!=='none' && !chip.matches(':hover')) hideChip(); }, 160); });\r\n })();\r\n zone.addEventListener('dragover', e=>{ if(e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.indexOf('text/json/elementdefintion')>=0){ e.preventDefault(); zone.style.background='rgba(255,255,255,.18)'; } });\r\n zone.addEventListener('dragleave', ()=>{ zone.style.background=''; });\r\n zone.addEventListener('drop', e=>{ e.preventDefault(); e.stopPropagation(); zone.style.background='';\r\n try{ const td=e.dataTransfer.getData('text/json/elementdefintion'); if(!td) return; const def=JSON.parse(td); const html=htmlFromDef(def);\r\n if(!cfgRoot.slots) cfgRoot.slots={}; cfgRoot.slots[key]=(cfgRoot.slots[key]||'')+html;\r\n if(window.IOB && window.IOB.saveConfig) window.IOB.saveConfig();\r\n }catch(err){ console.warn('[nav] slot drop failed', err); }\r\n });\r\n }\r\n const isDesigner = !!document.querySelector('iobroker-webui-app-shell');\r\n if(typeof isDesigner!=='undefined' && isDesigner){ try{ document.querySelectorAll('.nav-slot-delchip').forEach(n=>n.remove()); }catch(e){} }\r\n const hdr=instance._getDomElement('sbHdr'), items=instance._getDomElement('sbItems');\r\n if(cfg.bgColor) instance.style.setProperty('--sb-bg', cfg.bgColor);\r\n if(cfg.textColor){ instance.style.setProperty('--sb-fg', cfg.textColor); instance.style.setProperty('--mi-fg', cfg.textColor); }\r\n if(cfg.menuHeaderTextColor) instance.style.setProperty('--sb-hdr-fg', cfg.menuHeaderTextColor);\r\n if(cfg.bgColorSelected){ instance.style.setProperty('--mi-active', cfg.bgColorSelected); instance.style.setProperty('--mi-active-icon', cfg.bgColorSelected); }\r\n if(cfg.textColorSelected) instance.style.setProperty('--mi-active-fg', cfg.textColorSelected);\r\n if(hdr){ hdr.textContent=cfg.menuHeaderText||''; hdr.style.display=cfg.menuHeaderText?'':'none'; }\r\n // how the menu opens: full, icons only, hidden, or as this browser left it last time\r\n const stateKey='webui.nav.state.'+((IOB&&IOB.configPath)||'');\r\n const startAs=['expanded','icons','hidden','last'].includes(cfg.startState)?cfg.startState:'expanded';\r\n let state=startAs;\r\n if(startAs==='last'){ try{ state=localStorage.getItem(stateKey)||'expanded'; }catch(e){ state='expanded'; } }\r\n if(!['expanded','icons','hidden'].includes(state)) state='expanded';\r\n if(state==='hidden'&&cfg.doNotHideCompletely) state='icons'; // a phone: a side menu that starts open covers the screen — start closed, the ☰ opens it\n try{ const side=!cfg.position||cfg.position==='left'||cfg.position==='right'; if(side&&state==='expanded'&&window.matchMedia&&matchMedia('(max-width: 768px)').matches) state=cfg.doNotHideCompletely?'icons':'hidden'; }catch(e){}\r\n const ICONS_W=64; let burger=null; let hasItems=false;\r\n const navWidth = cfg.menuWidth ? (/^\\d+$/.test(String(cfg.menuWidth))?cfg.menuWidth+'px':String(cfg.menuWidth)) : '220px';\r\n // Groups: none (a flat list), 'folder' (the screen's folder: tests/pump → tests) or 'group'\r\n // (the screen's own Navigation → Group). A group opens and closes; a \"/\" nests it.\r\n const groupBy = cfg.groupBy || 'none';\r\n const groupsKey='webui.nav.groups.'+((IOB&&IOB.configPath)||'');\r\n let openGroups={}; try{ openGroups=JSON.parse(localStorage.getItem(groupsKey)||'{}')||{}; }catch(e){}\r\n const saveGroups=()=>{ try{ localStorage.setItem(groupsKey, JSON.stringify(openGroups)); }catch(e){} };\r\n const groupOf=(e)=> groupBy==='folder' ? (e.name.includes('/') ? e.name.slice(0, e.name.lastIndexOf('/')) : '') : groupBy==='group' ? String(e.group||'').replace(/^\\/+|\\/+$/g,'') : '';\r\n const setActive=(name)=>{\r\n if(!items) return;\r\n items.querySelectorAll('webui-navigation-menu-item').forEach(mi=>{ if(mi.kind!=='group') mi.selected=(mi.screenName===name); });\r\n // the group holding the open screen is opened\r\n const own=items.querySelector('webui-navigation-menu-item[data-entry=\"'+CSS.escape(String(name))+'\"]');\r\n for(let g=own&&own.parentElement; g&&g!==items; g=g.parentElement){ if(g.dataset.group!=null && g.hidden){ g.hidden=false; const head=g.previousElementSibling; if(head) head.expanded=true; } }\r\n };\r\n const navTo=(name,kind)=>{ if(IOB&&IOB.changeView) IOB.changeView.emit(kind==='report'?{name:name,type:'report'}:name); setActive(name); if(cfg.hideAfterSelection) setState('hidden'); };\r\n async function loadEntries(){\r\n if(!IOB||!IOB.getAllNames) return [];\r\n // Reports carry every screen setting, navigation among them, so they belong in the\r\n // menu exactly as screens do — the entry remembers which kind it is so the runtime opens\r\n // a .report in the report viewer rather than looking for a screen of that name.\r\n // Screens and reports are listed at the same time and every file is read at the same\r\n // time — each listing is a round trip to the server, so waiting for one before the next\r\n // is what made the menu slow.\r\n const kinds=await Promise.all(['screen','report'].map(async k=>[k,await IOB.getAllNames(k).catch(()=>[])]));\r\n const all=await Promise.all(kinds.flatMap(([kind,names])=>names.map(async n=>{ const o=await IOB.getWebuiObject(kind,n).catch(()=>null); const nv=o&&o.settings&&o.settings.navigation; return nv&&nv.show?{name:n,kind:kind,title:nv.title||n,order:parseFloat(nv.order)||0,image:nv.image||'',group:nv.group||'',badge:nv.badge||''}:null; })));\r\n const list=all.filter(Boolean);\r\n list.sort((a,b)=>a.order-b.order); return list;\r\n }\r\n // The menu this browser saw last is drawn at once; the fresh list replaces it when it differs.\r\n const menuCacheKey='webui.nav.menu.'+((IOB&&IOB.configPath)||'');\r\n async function render(){\r\n // items made before the menu item is defined lose what is set on them (level, image):\r\n // wait for the definition, at most 3 s\r\n if(window.customElements&&!customElements.get('webui-navigation-menu-item')) await Promise.race([customElements.whenDefined('webui-navigation-menu-item'), new Promise(r=>setTimeout(r,3000))]);\r\n let cached=null;\r\n if(!isDesigner){ try{ cached=JSON.parse(localStorage.getItem(menuCacheKey)||'null'); }catch(e){} if(Array.isArray(cached)) draw(cached); }\r\n const list=await loadEntries();\r\n const fresh=JSON.stringify(list);\r\n if(!Array.isArray(cached)||JSON.stringify(cached)!==fresh) draw(list);\r\n if(!isDesigner){ try{ localStorage.setItem(menuCacheKey,fresh); }catch(e){} }\r\n }\r\n function itemFor(e, level){\r\n const mi=document.createElement('webui-navigation-menu-item');\r\n mi.dataset.managed='1'; mi.dataset.entry=e.name;\r\n // inside its folder an entry without its own title shows its name without the folder\r\n mi.screenName=e.name; mi.title=(groupBy==='folder'&&e.title===e.name&&e.name.includes('/'))?e.name.slice(e.name.lastIndexOf('/')+1):e.title; mi.image=e.image; mi.level=level; mi.tint=cfg.tintIcons!==false; mi.compact=(state==='icons');\r\n mi.badge=badgeVals.has(e.name)?badgeVals.get(e.name):'';\r\n mi.addEventListener('menuitemclick',()=>navTo(e.name,e.kind));\r\n return mi;\r\n }\r\n // entries → a tree of groups; a group sits where its first entry (by order) sits\r\n function tree(list){\r\n const root={sub:new Map(),rows:[]};\r\n for(const e of list){\r\n let node=root, path='';\r\n for(const seg of groupOf(e).split('/').filter(Boolean)){\r\n path=path?path+'/'+seg:seg;\r\n if(!node.sub.has(seg)){ const g={title:seg,path:path,sub:new Map(),rows:[]}; node.sub.set(seg,g); node.rows.push({group:g,order:e.order}); }\r\n node=node.sub.get(seg);\r\n }\r\n node.rows.push({entry:e,order:e.order});\r\n }\r\n // a group with its own order (Settings → Navigation → Groups) moves there\r\n (function sortNode(n){ for(const r of n.rows) if(r.group){ const o=parseFloat(groupCfg(r.group.path).order); if(!isNaN(o)) r.order=o; } n.rows.sort((a,b)=>a.order-b.order); for(const g of n.sub.values()) sortNode(g); })(root);\r\n return root;\r\n }\r\n // Settings → Navigation → Groups: own title (a translation key works too), icon and order\r\n const groupCfg=(path)=>(cfg.groups&&cfg.groups[path])||{};\r\n const groupTitle=(g)=>{ const t=groupCfg(g.path).title||g.title; try{ const tr=IOB&&IOB.t?IOB.t(t):t; return tr&&tr!==t?tr:t; }catch(e){ return t; } };\r\n // Icons only: a group opens its entries in a panel beside the sidebar (hover or click).\r\n // The panel lives in the page (the sidebar clips what is inside it) and takes the menu's colours.\r\n let fly=null, flyT=0;\r\n const VARS=['--sb-bg','--sb-fg','--mi-bg','--mi-fg','--mi-hover','--mi-active','--mi-active-border','--mi-active-fg','--mi-active-icon'];\r\n function closeFlyout(){ clearTimeout(flyT); if(fly){ fly.remove(); fly=null; } }\r\n function closeSoon(){ clearTimeout(flyT); flyT=setTimeout(closeFlyout,280); }\r\n function openFlyout(head, g){\r\n clearTimeout(flyT);\r\n if(fly&&fly._g===g) return;\r\n closeFlyout();\r\n fly=document.createElement('div'); fly._g=g; fly.className='nav-flyout';\r\n const cs=getComputedStyle(instance); for(const v of VARS){ const x=cs.getPropertyValue(v); if(x&&x.trim()) fly.style.setProperty(v,x.trim()); }\r\n const r=head.getBoundingClientRect(), sb=instance.getBoundingClientRect(), top=Math.max(8,r.top-8);\r\n // beside a vertical menu (on its inner side), under a top bar, over a bottom bar\r\n const place=horiz?(pos==='top'?{left:Math.max(6,r.left)+'px',top:(sb.bottom+6)+'px',maxHeight:(innerHeight-sb.bottom-18)+'px'}:{left:Math.max(6,r.left)+'px',bottom:(innerHeight-sb.top+6)+'px',maxHeight:(sb.top-18)+'px'})\r\n :(pos==='right'?{right:(innerWidth-sb.left+6)+'px',top:top+'px',maxHeight:(innerHeight-top-12)+'px'}:{left:((state==='icons'?sb.left+ICONS_W:sb.right)+6)+'px',top:top+'px',maxHeight:(innerHeight-top-12)+'px'});\r\n Object.assign(fly.style,place,{position:'fixed',zIndex:'100001',minWidth:'210px',maxWidth:'320px',overflow:'auto',padding:'6px 0 8px',borderRadius:'12px',background:'var(--sb-bg,linear-gradient(180deg,#1e3c72 0%,#2a5298 100%))',color:'var(--sb-fg,rgba(255,255,255,.9))',boxShadow:'0 10px 30px rgba(0,0,0,.38)'});\r\n const h=document.createElement('div'); h.textContent=groupTitle(g); Object.assign(h.style,{padding:'8px 16px 6px',fontWeight:'700',fontSize:'13px',opacity:'.85',whiteSpace:'nowrap'}); fly.appendChild(h);\r\n const current=(document.getElementById('viewer')||{}).screenName;\r\n (function add(node, level){ for(const row of node.rows){\r\n if(row.entry){ const mi=itemFor(row.entry, level); mi.compact=false; mi.selected=(row.entry.name===current); mi.addEventListener('menuitemclick',closeFlyout); fly.appendChild(mi); }\r\n else { const sub=document.createElement('webui-navigation-menu-item'); sub.dataset.managed='1'; sub.kind='group'; sub.expanded=true; sub.title=groupTitle(row.group); sub.image=groupCfg(row.group.path).image||''; sub.level=level; sub.tint=cfg.tintIcons!==false; fly.appendChild(sub); add(row.group, level+1); }\r\n } })(g, 0);\r\n fly.addEventListener('mouseenter',()=>clearTimeout(flyT)); fly.addEventListener('mouseleave',closeSoon);\r\n document.body.appendChild(fly);\r\n }\r\n if(!isDesigner){\r\n // the runtime rebuilds the sidebar when the settings change: a removed sidebar lets go\r\n const onDown=e=>{ if(!instance.isConnected){ document.removeEventListener('pointerdown',onDown,true); closeFlyout(); return; } if(fly&&!e.composedPath().some(n=>n===fly||n===instance)) closeFlyout(); };\r\n const onKey=e=>{ if(!instance.isConnected){ document.removeEventListener('keydown',onKey); return; } if(e.key==='Escape') closeFlyout(); };\r\n document.addEventListener('pointerdown',onDown,true);\r\n document.addEventListener('keydown',onKey);\r\n }\r\n function drawNode(node, parent, level){\r\n for(const r of node.rows){\r\n if(r.entry){ parent.appendChild(itemFor(r.entry, level)); continue; }\r\n const g=r.group, head=document.createElement('webui-navigation-menu-item'), box=document.createElement('div');\r\n head.dataset.managed='1'; head.kind='group'; head.title=groupTitle(g); head.image=groupCfg(g.path).image||''; head.level=level; head.tint=cfg.tintIcons!==false; head.compact=(state==='icons');\r\n head.expanded=!!openGroups[g.path];\r\n box.dataset.group=g.path; box.hidden=!openGroups[g.path];\r\n head.addEventListener('menugroupclick',()=>{ if(state==='icons'||horiz){ fly&&fly._g===g?closeFlyout():openFlyout(head,g); return; } box.hidden=!box.hidden; head.expanded=!box.hidden; openGroups[g.path]=!box.hidden; saveGroups(); });\r\n head.addEventListener('mouseenter',()=>{ if(state==='icons'||horiz) openFlyout(head,g); });\r\n head.addEventListener('mouseleave',()=>{ if(state==='icons'||horiz) closeSoon(); });\r\n head._entries=(function all(n){ return n.rows.flatMap(x=>x.entry?[x.entry.name]:all(x.group)); })(g);\r\n parent.appendChild(head); parent.appendChild(box);\r\n drawNode(g, box, level+1);\r\n }\r\n }\r\n // Badge: a screen's Navigation → Badge names a state; its value shows on the entry (a number,\r\n // or a dot for true) and adds up on the group that holds it — an alarm count, for example.\r\n const badgeVals=new Map(); let badgeSubs=[];\r\n const badgeOf=(v)=>{ if(v===true||v==='true') return true; const n=Number(v); if(v!==''&&v!=null&&!isNaN(n)) return n>0?n:''; return (v==null||v===false||v==='false')?'':String(v); };\r\n function showBadge(name){\r\n const v=badgeVals.has(name)?badgeVals.get(name):'';\r\n const sel='webui-navigation-menu-item[data-entry=\"'+CSS.escape(String(name))+'\"]';\r\n for(const root of [items,fly]) if(root) root.querySelectorAll(sel).forEach(mi=>{ mi.badge=v; });\r\n // groups: the sum of their numbers, or a dot when only dots\r\n const heads=[]; for(const root of [items,fly]) if(root) root.querySelectorAll('webui-navigation-menu-item').forEach(h=>{ if(h.kind==='group'&&h._entries) heads.push(h); });\r\n for(const h of heads){ let sum=0, dot=false; for(const n of h._entries){ const x=badgeVals.get(n); if(typeof x==='number') sum+=x; else if(x) dot=true; } h.badge=sum>0?sum:(dot?true:''); }\r\n }\r\n function watchBadges(list){\r\n if(isDesigner||!IOB||!IOB.subscribeState) return;\r\n for(const [id,cb] of badgeSubs) try{ IOB.unsubscribeState(id,cb); }catch(e){}\r\n badgeSubs=[];\r\n for(const e of list){\r\n if(!e.badge) continue;\r\n const cb=(_id,st)=>{ if(!instance.isConnected){ for(const [i,c] of badgeSubs) try{ IOB.unsubscribeState(i,c); }catch(x){} badgeSubs=[]; closeFlyout(); return; } badgeVals.set(e.name,badgeOf(st?st.val:'')); showBadge(e.name); };\r\n IOB.subscribeState(e.badge,cb); badgeSubs.push([e.badge,cb]);\r\n Promise.resolve(IOB.getState(e.badge)).then(st=>cb(e.badge,st)).catch(()=>{});\r\n }\r\n }\r\n function draw(list){\r\n hasItems=list.length>0;\r\n if(items){ items.innerHTML='';\r\n if(groupBy==='none') for(const e of list) items.appendChild(itemFor(e, 0));\r\n else drawNode(tree(list), items, 0);\r\n const sv=document.getElementById('viewer'); if(sv&&sv.screenName) setActive(sv.screenName);\r\n }\r\n watchBadges(list);\r\n if(!isDesigner){ if(hasItems && !cfg.hideMenu) applyRuntimeLayout(); else { const v=document.getElementById('viewer'); if(v){ v.style.left='0'; v.style.width='100%'; } instance.style.display='none'; } }\r\n }\r\n // Position (left | right | top | bottom) and layout (push: the screen makes room; overlay: the\r\n // menu opens over the screen, which keeps its size). An older \"horizontal\" orientation\r\n // without a position means top. Top and bottom are a bar: groups open in a panel.\r\n const pos=['left','right','top','bottom'].includes(cfg.position)?cfg.position:(cfg.orientation==='horizontal'?'top':'left');\r\n const horiz=(pos==='top'||pos==='bottom');\r\n const overlay=(cfg.layout==='overlay');\r\n const BAR_H=56, BAR_ICONS_H=44;\r\n if(horiz) instance.setAttribute('data-orient','h'); else instance.removeAttribute('data-orient');\r\n let barTop=0;\r\n function applyRuntimeLayout(){\r\n const ab=document.getElementById('__nav-appbar-host'); barTop=ab?ab.offsetHeight:0;\r\n const st=instance.style; st.position='fixed'; st.zIndex='99999'; st.overflow='hidden';\r\n st.left=st.right=st.top=st.bottom='';\r\n if(pos==='left'){ st.left='0'; st.top='0'; st.bottom='0'; }\r\n else if(pos==='right'){ st.right='0'; st.top='0'; st.bottom='0'; }\r\n else if(pos==='top'){ st.left='0'; st.right='0'; st.top=barTop+'px'; }\r\n else { st.left='0'; st.right='0'; st.bottom='0'; }\r\n if(!burger){\r\n burger=document.createElement('div'); burger.id='__nav-burger'; burger.textContent='☰';\r\n burger.setAttribute('role','button'); burger.tabIndex=0; burger.setAttribute('aria-label','Menu'); burger.title='Menu';\r\n burger.style.cssText='position:fixed;z-index:100002;display:flex;align-items:center;justify-content:center;font-size:22px;cursor:pointer;box-sizing:border-box;user-select:none;';\r\n burger.onclick=cycle;\r\n burger.onkeydown=(e)=>{ if(e.key==='Enter'||e.key===' '){ e.preventDefault(); cycle(); } };\r\n document.body.appendChild(burger);\r\n }\r\n // the app bar keeps room for the ☰ on the side it sits on\r\n padBar();\r\n // a bar without an app bar: the floating ☰ needs room at its start\r\n // a bar without an app bar: the floating ☰ needs room at its start\r\n st.paddingLeft=(pos==='top'&&!barTop)?'56px':'';\r\n setState(state);\r\n }\r\n // The sidebar and the screen move together, with the same easing: the screen's size\r\n // changes every frame, so a stretched screen scales smoothly instead of jumping. The first\r\n // placement is not animated.\r\n const EASE='.32s cubic-bezier(.22,.8,.24,1)';\r\n const still=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;\r\n let placed=false;\r\n function setState(s){\r\n closeFlyout();\r\n state=s;\r\n if(startAs==='last'&&!isDesigner){ try{ localStorage.setItem(stateKey,s); }catch(e){} } const v=document.getElementById('viewer');\r\n const anim=placed&&!still; placed=true;\r\n instance.style.transition=anim?(horiz?'height ':'width ')+EASE+',transform '+EASE:'none';\r\n if(v) v.style.transition=anim?['left','top','width','height'].map(p=>p+' '+EASE).join(','):'none';\r\n instance.setAttribute('data-state', state); padBar();\r\n const compact=(state==='icons');\r\n if(items) items.querySelectorAll('webui-navigation-menu-item').forEach(mi=>{ mi.compact=compact; mi.toggleAttribute('data-h',horiz); });\r\n // icons only, and any bar: a group's entries open in the panel, not under it\r\n if(items) items.querySelectorAll('div[data-group]').forEach(box=>{ box.style.display=(compact||horiz)?'none':''; });\r\n const hidden=(state==='hidden');\r\n let size='0px';\r\n if(state==='expanded'){ size=horiz?BAR_H+'px':navWidth; if(hdr)hdr.style.display=cfg.menuHeaderText?'':'none'; }\r\n else if(state==='icons'){ size=(horiz?BAR_ICONS_H:ICONS_W)+'px'; if(hdr)hdr.style.display='none'; }\r\n if(horiz){ instance.style.width=''; instance.style.height=size; instance.style.transform=''; }\r\n else { instance.style.height=''; if(!hidden) instance.style.width=size; instance.style.transform=hidden?(pos==='right'?'translateX(110%)':'translateX(-110%)'):'translateX(0)'; }\r\n if(v){\r\n const room=(overlay||hidden)?'0px':size;\r\n let l='0px', t=barTop+'px', w='100%', h='calc(100% - '+barTop+'px)';\r\n if(pos==='left'){ l=room; w='calc(100% - '+room+')'; }\r\n else if(pos==='right'){ w='calc(100% - '+room+')'; }\r\n else if(pos==='top'){ t='calc('+barTop+'px + '+room+')'; h='calc(100% - '+barTop+'px - '+room+')'; }\r\n else { h='calc(100% - '+barTop+'px - '+room+')'; }\r\n v.style.left=l; v.style.top=t; v.style.width=w; v.style.height=h;\r\n }\r\n if(!horiz){\r\n const row=(barTop||52)+'px';\r\n if(hdr){ const h=hdr.style; h.display='flex'; h.alignItems='center'; h.boxSizing='border-box'; h.height=h.minHeight=row; h.paddingTop=h.paddingBottom='0';\r\n if(pos==='right'){ h.paddingLeft='calc('+row+' + 8px)'; h.paddingRight='16px'; } else { h.paddingRight='calc('+row+' + 8px)'; h.paddingLeft='16px'; }\r\n h.visibility=(state==='icons')?'hidden':''; }\r\n const ab=document.getElementById('__nav-appbar-host');\r\n if(ab){ ab.style.transition=anim?'left '+EASE+',right '+EASE:'none'; const off=hidden?'0px':size;\r\n if(pos==='right'){ ab.style.right=off; ab.style.left='0'; } else { ab.style.left=off; ab.style.right='0'; } }\r\n } else if(pos==='bottom'){ instance.style.paddingLeft=hidden?'':size; }\r\n placeBurger(size, hidden, anim);\r\n }\r\n // the app bar keeps room for the ☰ on the side it sits on — a vertical menu only while it is hidden\r\n function padBar(){\r\n const ab=document.getElementById('__nav-appbar-host'), abEl=ab&&ab.querySelector('webui-navigation-app-bar');\r\n const docked=(pos==='top')||(state==='hidden'&&pos!=='bottom');\r\n const room=docked?(barTop+12)+'px':'16px';\r\n if(abEl) abEl.style.setProperty('--bar-pad', pos==='right'?'0 '+room+' 0 16px':'0 16px 0 '+room);\r\n }\r\n // The ☰ goes with the menu. A side menu runs the full height and carries it in its own top\r\n // row, in the corner towards the screen (centred on the icon strip), in the menu's colours;\r\n // hidden, the ☰ goes back to the app bar's corner. A top bar keeps it in the app bar's corner;\r\n // a bottom bar at its start.\r\n function placeBurger(size, hidden, anim){\r\n if(!burger) return;\r\n const b=burger.style, row=barTop||52;\r\n b.left=b.right=b.top=b.bottom='';\r\n b.transition=anim?['left','right','top','bottom','width','height'].map(p=>p+' '+EASE).join(','):'none';\r\n b.width=b.height=row+'px'; b.borderRadius='0'; b.background=cfg.showButtonBg||'transparent';\r\n let fg=cfg.showButtonColor;\r\n const menuFg=()=>{ try{ return getComputedStyle(hdr||instance).color; }catch(e){ return ''; } };\r\n if(!horiz&&!hidden){\r\n if(!fg) fg=menuFg();\r\n const off=state==='icons'?'calc(('+size+' - '+row+'px) / 2)':'calc('+size+' - '+row+'px)';\r\n b.top='0'; if(pos==='right') b.right=off; else b.left=off;\r\n } else if(pos==='bottom'){\r\n if(hidden){ b.bottom='8px'; b.left='8px'; b.width=b.height='42px'; b.borderRadius='8px'; b.background=cfg.showButtonBg||'rgba(0,0,0,.45)'; }\r\n else { if(!fg) fg=menuFg(); b.bottom='0'; b.left='0'; b.width=b.height=size; }\r\n } else if(barTop){ b.top='0'; if(pos==='right') b.right='0'; else b.left='0'; }\r\n else { b.top='8px'; if(pos==='right') b.right='8px'; else b.left='8px'; b.width=b.height='42px'; b.borderRadius='8px'; b.background=cfg.showButtonBg||'rgba(0,0,0,.45)'; }\r\n b.color=fg||'#fff';\r\n }\r\n if(pos==='right') instance.setAttribute('data-pos','right'); else instance.removeAttribute('data-pos');\r\n function cycle(){ const o=['expanded','icons','hidden']; let n=o[(o.indexOf(state)+1)%o.length]; if(n==='hidden'&&cfg.doNotHideCompletely)n='icons'; setState(n); }\r\n instance.setAttribute('data-state', state);\r\n wireSlot(instance._getDomElement('slotTop'), cfg, 'top');\r\n wireSlot(instance._getDomElement('slotBottom'), cfg, 'bottom');\r\n render();\r\n if(IOB&&IOB.changeView) IOB.changeView.on(v=>setActive(v));\r\n const sv=document.getElementById('viewer'); if(sv&&sv.screenName) setActive(sv.screenName);\r\n}"}