iobroker.mywebui 1.173.1 → 1.176.0

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 (253) hide show
  1. package/README.md +15 -0
  2. package/default-controls/controls/dashboard/boiler.control +51 -0
  3. package/default-controls/controls/dashboard/command-group.control +19 -0
  4. package/default-controls/controls/dashboard/conveyor-box.control +19 -0
  5. package/default-controls/controls/dashboard/conveyor.control +35 -0
  6. package/default-controls/controls/dashboard/countdown-tile.control +32 -0
  7. package/default-controls/controls/dashboard/dial-gauge.control +39 -0
  8. package/default-controls/controls/dashboard/kpi-tile.control +52 -0
  9. package/default-controls/controls/dashboard/logic-gate.control +40 -0
  10. package/default-controls/controls/dashboard/notification-bell.control +19 -0
  11. package/default-controls/controls/dashboard/panel-card.control +45 -0
  12. package/default-controls/controls/dashboard/setpoint-slider.control +47 -0
  13. package/default-controls/controls/dashboard/status-chip.control +74 -0
  14. package/default-controls/controls/dashboard/time-spinner.control +37 -0
  15. package/default-controls/controls/navigation/app-bar.control +1 -1
  16. package/default-controls/controls/navigation/sidebar.control +1 -1
  17. package/dist/backend/3d-editor-integration.js +1 -1
  18. package/dist/backend/main.js +1 -1
  19. package/dist/backend/protectedWrite.js +1 -1
  20. package/dist/backend/reportCsv.js +1 -1
  21. package/dist/backend/reportGenerate.js +1 -1
  22. package/dist/backend/reportOutput.js +1 -1
  23. package/dist/backend/reportPdf.js +1 -1
  24. package/dist/backend/reportRenderer.js +1 -1
  25. package/dist/backend/reportSandbox.js +1 -1
  26. package/dist/backend/reportSandboxWorker.js +1 -1
  27. package/docs/README.md +1 -0
  28. package/docs/bindings.md +4 -0
  29. package/docs/reports.md +7 -0
  30. package/docs/scada-controls.md +47 -0
  31. package/docs/screens.md +2 -2
  32. package/io-package.json +1 -1
  33. package/native/mywebui_scada_engine.win32-x64-msvc.node +0 -0
  34. package/package.json +1 -1
  35. package/www/3d-editor/js/Animation.js +1 -1
  36. package/www/3d-editor/js/AnimationResizer.js +1 -1
  37. package/www/3d-editor/js/Command.js +1 -1
  38. package/www/3d-editor/js/Config.js +1 -1
  39. package/www/3d-editor/js/Editor.js +1 -1
  40. package/www/3d-editor/js/EditorControls.js +1 -1
  41. package/www/3d-editor/js/GLTFImportDialog.js +1 -1
  42. package/www/3d-editor/js/History.js +1 -1
  43. package/www/3d-editor/js/Loader.js +1 -1
  44. package/www/3d-editor/js/LoaderUtils.js +1 -1
  45. package/www/3d-editor/js/Menubar.Add.js +1 -1
  46. package/www/3d-editor/js/Menubar.Edit.js +1 -1
  47. package/www/3d-editor/js/Menubar.File.js +1 -1
  48. package/www/3d-editor/js/Menubar.Help.js +1 -1
  49. package/www/3d-editor/js/Menubar.Render.js +1 -1
  50. package/www/3d-editor/js/Menubar.Status.js +1 -1
  51. package/www/3d-editor/js/Menubar.View.js +1 -1
  52. package/www/3d-editor/js/Menubar.js +1 -1
  53. package/www/3d-editor/js/Player.js +1 -1
  54. package/www/3d-editor/js/Resizer.js +1 -1
  55. package/www/3d-editor/js/Script.js +1 -1
  56. package/www/3d-editor/js/Selector.js +1 -1
  57. package/www/3d-editor/js/Sidebar.Geometry.BoxGeometry.js +1 -1
  58. package/www/3d-editor/js/Sidebar.Geometry.BufferGeometry.js +1 -1
  59. package/www/3d-editor/js/Sidebar.Geometry.CapsuleGeometry.js +1 -1
  60. package/www/3d-editor/js/Sidebar.Geometry.CircleGeometry.js +1 -1
  61. package/www/3d-editor/js/Sidebar.Geometry.CylinderGeometry.js +1 -1
  62. package/www/3d-editor/js/Sidebar.Geometry.DodecahedronGeometry.js +1 -1
  63. package/www/3d-editor/js/Sidebar.Geometry.ExtrudeGeometry.js +1 -1
  64. package/www/3d-editor/js/Sidebar.Geometry.IcosahedronGeometry.js +1 -1
  65. package/www/3d-editor/js/Sidebar.Geometry.LatheGeometry.js +1 -1
  66. package/www/3d-editor/js/Sidebar.Geometry.Modifiers.js +1 -1
  67. package/www/3d-editor/js/Sidebar.Geometry.OctahedronGeometry.js +1 -1
  68. package/www/3d-editor/js/Sidebar.Geometry.PlaneGeometry.js +1 -1
  69. package/www/3d-editor/js/Sidebar.Geometry.RingGeometry.js +1 -1
  70. package/www/3d-editor/js/Sidebar.Geometry.ShapeGeometry.js +1 -1
  71. package/www/3d-editor/js/Sidebar.Geometry.SphereGeometry.js +1 -1
  72. package/www/3d-editor/js/Sidebar.Geometry.TetrahedronGeometry.js +1 -1
  73. package/www/3d-editor/js/Sidebar.Geometry.TextGeometry.js +1 -1
  74. package/www/3d-editor/js/Sidebar.Geometry.TorusGeometry.js +1 -1
  75. package/www/3d-editor/js/Sidebar.Geometry.TorusKnotGeometry.js +1 -1
  76. package/www/3d-editor/js/Sidebar.Geometry.TubeGeometry.js +1 -1
  77. package/www/3d-editor/js/Sidebar.Geometry.js +1 -1
  78. package/www/3d-editor/js/Sidebar.Material.BooleanProperty.js +1 -1
  79. package/www/3d-editor/js/Sidebar.Material.ColorProperty.js +1 -1
  80. package/www/3d-editor/js/Sidebar.Material.ConstantProperty.js +1 -1
  81. package/www/3d-editor/js/Sidebar.Material.MapProperty.js +1 -1
  82. package/www/3d-editor/js/Sidebar.Material.NumberProperty.js +1 -1
  83. package/www/3d-editor/js/Sidebar.Material.Program.js +1 -1
  84. package/www/3d-editor/js/Sidebar.Material.RangeValueProperty.js +1 -1
  85. package/www/3d-editor/js/Sidebar.Material.js +1 -1
  86. package/www/3d-editor/js/Sidebar.Object.js +1 -1
  87. package/www/3d-editor/js/Sidebar.Project.App.js +1 -1
  88. package/www/3d-editor/js/Sidebar.Project.Materials.js +1 -1
  89. package/www/3d-editor/js/Sidebar.Project.Renderer.js +1 -1
  90. package/www/3d-editor/js/Sidebar.Project.Resources.js +1 -1
  91. package/www/3d-editor/js/Sidebar.Project.js +1 -1
  92. package/www/3d-editor/js/Sidebar.Properties.js +1 -1
  93. package/www/3d-editor/js/Sidebar.Scene.js +1 -1
  94. package/www/3d-editor/js/Sidebar.Script.js +1 -1
  95. package/www/3d-editor/js/Sidebar.Settings.History.js +1 -1
  96. package/www/3d-editor/js/Sidebar.Settings.Shortcuts.js +1 -1
  97. package/www/3d-editor/js/Sidebar.Settings.js +1 -1
  98. package/www/3d-editor/js/Sidebar.js +1 -1
  99. package/www/3d-editor/js/Storage.js +1 -1
  100. package/www/3d-editor/js/Strings.js +1 -1
  101. package/www/3d-editor/js/TextureParametersDialog.js +1 -1
  102. package/www/3d-editor/js/Toolbar.js +1 -1
  103. package/www/3d-editor/js/Viewport.Controls.js +1 -1
  104. package/www/3d-editor/js/Viewport.Info.js +1 -1
  105. package/www/3d-editor/js/Viewport.Pathtracer.js +1 -1
  106. package/www/3d-editor/js/Viewport.ViewHelper.js +1 -1
  107. package/www/3d-editor/js/Viewport.XR.js +1 -1
  108. package/www/3d-editor/js/Viewport.js +1 -1
  109. package/www/3d-editor/js/commands/AddObjectCommand.js +1 -1
  110. package/www/3d-editor/js/commands/AddScriptCommand.js +1 -1
  111. package/www/3d-editor/js/commands/Commands.js +1 -1
  112. package/www/3d-editor/js/commands/MoveObjectCommand.js +1 -1
  113. package/www/3d-editor/js/commands/MultiCmdsCommand.js +1 -1
  114. package/www/3d-editor/js/commands/RemoveObjectCommand.js +1 -1
  115. package/www/3d-editor/js/commands/RemoveScriptCommand.js +1 -1
  116. package/www/3d-editor/js/commands/SetColorCommand.js +1 -1
  117. package/www/3d-editor/js/commands/SetGeometryCommand.js +1 -1
  118. package/www/3d-editor/js/commands/SetGeometryValueCommand.js +1 -1
  119. package/www/3d-editor/js/commands/SetMaterialColorCommand.js +1 -1
  120. package/www/3d-editor/js/commands/SetMaterialCommand.js +1 -1
  121. package/www/3d-editor/js/commands/SetMaterialMapCommand.js +1 -1
  122. package/www/3d-editor/js/commands/SetMaterialRangeCommand.js +1 -1
  123. package/www/3d-editor/js/commands/SetMaterialValueCommand.js +1 -1
  124. package/www/3d-editor/js/commands/SetMaterialVectorCommand.js +1 -1
  125. package/www/3d-editor/js/commands/SetPositionCommand.js +1 -1
  126. package/www/3d-editor/js/commands/SetRotationCommand.js +1 -1
  127. package/www/3d-editor/js/commands/SetScaleCommand.js +1 -1
  128. package/www/3d-editor/js/commands/SetSceneCommand.js +1 -1
  129. package/www/3d-editor/js/commands/SetScriptValueCommand.js +1 -1
  130. package/www/3d-editor/js/commands/SetShadowValueCommand.js +1 -1
  131. package/www/3d-editor/js/commands/SetTextureParametersCommand.js +1 -1
  132. package/www/3d-editor/js/commands/SetUuidCommand.js +1 -1
  133. package/www/3d-editor/js/commands/SetValueCommand.js +1 -1
  134. package/www/dist/frontend/bundle/HistoryDirectives-7WFU2ZTM.js +1 -0
  135. package/www/dist/frontend/bundle/ReportViewer-WPWE2MW6.js +197 -0
  136. package/www/dist/frontend/bundle/Sha256-BSH7CIPW.js +1 -1
  137. package/www/dist/frontend/bundle/chunk-CVWTTI5E.js +1 -0
  138. package/www/dist/frontend/bundle/chunk-KVPT4J7H.js +1 -1
  139. package/www/dist/frontend/bundle/chunk-NPDVKA2F.js +21 -0
  140. package/www/dist/frontend/bundle/common/IobrokerHandler.js +1 -1
  141. package/www/dist/frontend/bundle/runtime/CustomControls.js +1 -1
  142. package/www/dist/frontend/bundle/runtime/ScreenViewer.js +1 -1
  143. package/www/dist/frontend/bundle/runtime/controls.js +4 -4
  144. package/www/dist/frontend/bundle/runtime/init.js +2 -2
  145. package/www/dist/frontend/common/BindingExpression.js +1 -1
  146. package/www/dist/frontend/common/BuiltinTranslations.js +1 -1
  147. package/www/dist/frontend/common/Common.globals.js +1 -1
  148. package/www/dist/frontend/common/ConfirmWrite.js +1 -1
  149. package/www/dist/frontend/common/EChartsOption.js +1 -1
  150. package/www/dist/frontend/common/HistoryDirective.js +1 -1
  151. package/www/dist/frontend/common/HistoryOptions.js +1 -1
  152. package/www/dist/frontend/common/HistoryQuery.js +1 -1
  153. package/www/dist/frontend/common/HistoryQueryViews.js +1 -1
  154. package/www/dist/frontend/common/IobrokerHandler.js +1 -1
  155. package/www/dist/frontend/common/PlantPaxResolve.js +1 -1
  156. package/www/dist/frontend/common/ReadQuality.js +1 -1
  157. package/www/dist/frontend/common/ReportAllowlist.js +1 -1
  158. package/www/dist/frontend/common/ReportSchedule.js +1 -1
  159. package/www/dist/frontend/common/ReportTheme.js +1 -1
  160. package/www/dist/frontend/common/Runtime.js +1 -1
  161. package/www/dist/frontend/common/Sha256.js +1 -1
  162. package/www/dist/frontend/common/StateDiagnostics.js +1 -1
  163. package/www/dist/frontend/common/StateQuality.js +1 -1
  164. package/www/dist/frontend/common/WriteFeedback.js +1 -1
  165. package/www/dist/frontend/config/CommandHandling.js +1 -1
  166. package/www/dist/frontend/config/ConfigureWebcomponentDesigner.js +1 -1
  167. package/www/dist/frontend/config/ConfirmWindow.js +1 -1
  168. package/www/dist/frontend/config/DockHelper.js +1 -1
  169. package/www/dist/frontend/config/HistoryQueryWindow.js +1 -1
  170. package/www/dist/frontend/config/IobrokerWebui3DDriveEngine.js +1 -1
  171. package/www/dist/frontend/config/IobrokerWebui3DScreenEditor.js +3 -3
  172. package/www/dist/frontend/config/IobrokerWebui3DScreenPropertiesPanel.js +3 -3
  173. package/www/dist/frontend/config/IobrokerWebui3DScreenViewer.js +3 -3
  174. package/www/dist/frontend/config/IobrokerWebuiAppShell.js +3 -3
  175. package/www/dist/frontend/config/IobrokerWebuiBindingsEditor.js +1 -1
  176. package/www/dist/frontend/config/IobrokerWebuiConfirmationWrapper.js +3 -3
  177. package/www/dist/frontend/config/IobrokerWebuiControlPropertiesEditor.js +3 -3
  178. package/www/dist/frontend/config/IobrokerWebuiDynamicPropsEditor.js +1 -1
  179. package/www/dist/frontend/config/IobrokerWebuiEventAssignment.js +1 -1
  180. package/www/dist/frontend/config/IobrokerWebuiIconsView.js +3 -3
  181. package/www/dist/frontend/config/IobrokerWebuiMonacoEditor.js +3 -3
  182. package/www/dist/frontend/config/IobrokerWebuiPlantPaxTypesEditor.js +1 -1
  183. package/www/dist/frontend/config/IobrokerWebuiPropertyGrid.js +1 -1
  184. package/www/dist/frontend/config/IobrokerWebuiScreenEditor.js +1 -1
  185. package/www/dist/frontend/config/IobrokerWebuiScreensView.js +3 -3
  186. package/www/dist/frontend/config/IobrokerWebuiSecurityEditor.js +1 -1
  187. package/www/dist/frontend/config/IobrokerWebuiSignalPropertyEditor.js +1 -1
  188. package/www/dist/frontend/config/IobrokerWebuiSolutionExplorer.js +3 -3
  189. package/www/dist/frontend/config/IobrokerWebuiTranslationEditor.js +1 -1
  190. package/www/dist/frontend/config/IobrokerWebuiWidgetGallery.js +3 -3
  191. package/www/dist/frontend/config/RightDockStrip.js +1 -1
  192. package/www/dist/frontend/config/UprightOverlays.js +1 -1
  193. package/www/dist/frontend/helper/DialogHelper.js +3 -3
  194. package/www/dist/frontend/helper/EditorOfDesignItem.js +1 -1
  195. package/www/dist/frontend/helper/EsprimaHelper.js +1 -1
  196. package/www/dist/frontend/helper/Helper.js +1 -1
  197. package/www/dist/frontend/helper/LayoutHelper.js +1 -1
  198. package/www/dist/frontend/helper/XmlHelper.js +1 -1
  199. package/www/dist/frontend/helper/ZipHelper.js +1 -1
  200. package/www/dist/frontend/runtime/AnimationService.js +1 -1
  201. package/www/dist/frontend/runtime/CustomControls.js +1 -1
  202. package/www/dist/frontend/runtime/DynamicElementProperties.js +1 -1
  203. package/www/dist/frontend/runtime/EChart.js +1 -1
  204. package/www/dist/frontend/runtime/HabPanelLikeMenu.js +3 -3
  205. package/www/dist/frontend/runtime/HistoryDirectives.js +1 -1
  206. package/www/dist/frontend/runtime/NavigationShell.js +1 -1
  207. package/www/dist/frontend/runtime/ReportViewer.js +3 -3
  208. package/www/dist/frontend/runtime/RuntimeTheme.js +1 -1
  209. package/www/dist/frontend/runtime/ScreenViewer.js +3 -3
  210. package/www/dist/frontend/runtime/TranslateableText.js +2 -2
  211. package/www/dist/frontend/runtime/VisibilityService.js +1 -1
  212. package/www/dist/frontend/runtime/controls.js +1 -1
  213. package/www/dist/frontend/runtime/init.js +1 -1
  214. package/www/dist/frontend/scripting/IobrokerWebuiScriptSystem.js +1 -1
  215. package/www/dist/frontend/scripting/blockly/OpenScreen.js +1 -1
  216. package/www/dist/frontend/scripting/blockly/webuiBlocklyToolbox.js +1 -1
  217. package/www/dist/frontend/services/DynamicPropertiesHelper.js +1 -1
  218. package/www/dist/frontend/services/IobrokerWebuiBindableLocalObjectsService.js +1 -1
  219. package/www/dist/frontend/services/IobrokerWebuiBindableObjectDragDropService.js +1 -1
  220. package/www/dist/frontend/services/IobrokerWebuiBindableObjectsForPropertiesService.js +1 -1
  221. package/www/dist/frontend/services/IobrokerWebuiBindableObjectsService.js +1 -1
  222. package/www/dist/frontend/services/IobrokerWebuiConfigButtonProvider.js +1 -1
  223. package/www/dist/frontend/services/IobrokerWebuiCopyPasteService.js +1 -1
  224. package/www/dist/frontend/services/IobrokerWebuiCustomControlEventsService.js +1 -1
  225. package/www/dist/frontend/services/IobrokerWebuiCustomElementContextMenu.js +1 -1
  226. package/www/dist/frontend/services/IobrokerWebuiDemoProviderService.js +1 -1
  227. package/www/dist/frontend/services/IobrokerWebuiDynamicPropertiesService.js +1 -1
  228. package/www/dist/frontend/services/IobrokerWebuiEChartPropertiesService.js +1 -1
  229. package/www/dist/frontend/services/IobrokerWebuiEventsService.js +1 -1
  230. package/www/dist/frontend/services/IobrokerWebuiExternalDragDropService.js +1 -1
  231. package/www/dist/frontend/services/IobrokerWebuiLitPropertiesService.js +1 -1
  232. package/www/dist/frontend/services/IobrokerWebuiModelCommandService.js +1 -1
  233. package/www/dist/frontend/services/IobrokerWebuiPropertiesService.js +1 -1
  234. package/www/dist/frontend/services/IobrokerWebuiPropertyGroupsService.js +1 -1
  235. package/www/dist/frontend/services/IobrokerWebuiRefactorService.js +1 -1
  236. package/www/dist/frontend/services/IobrokerWebuiReportContextMenu.js +1 -1
  237. package/www/dist/frontend/services/IobrokerWebuiScreenContextMenu.js +1 -1
  238. package/www/dist/frontend/services/IobrokerWebuiSpecialPropertiesService.js +1 -1
  239. package/www/dist/frontend/services/IobrokerWebuiSpectrumEditorService.js +1 -1
  240. package/www/dist/frontend/services/IobrokerWebuiVisibilityPropertiesService.js +1 -1
  241. package/www/dist/frontend/services/LayoutComponentsPropertiesService.js +1 -1
  242. package/www/dist/frontend/widgets/OrthoFlowLineTool.js +1 -1
  243. package/www/dist/frontend/widgets/ReportToolbox.js +2 -2
  244. package/www/dist/frontend/widgets/customElementsObserver.js +1 -1
  245. package/www/dist/frontend/widgets/importWidgetFiles.js +1 -1
  246. package/www/dist/frontend/widgets/importWidgetFilesRuntime.js +1 -1
  247. package/www/dist/frontend/widgets/layout-components.js +1 -1
  248. package/www/dist/frontend/widgets/layout-placement-services.js +1 -1
  249. package/www/dist/frontend/widgets/testElement.js +1 -1
  250. package/www/dist/frontend/bundle/HistoryDirectives-2D3MY36Q.js +0 -1
  251. package/www/dist/frontend/bundle/ReportViewer-VIW3KLIA.js +0 -197
  252. package/www/dist/frontend/bundle/chunk-IFBVYILO.js +0 -1
  253. package/www/dist/frontend/bundle/chunk-YBPYQWW6.js +0 -21
package/README.md CHANGED
@@ -302,6 +302,21 @@ runtime.html?screenName=screen2
302
302
  Placeholder for next versions:
303
303
  ### __WORK IN PROGRESS__
304
304
  -->
305
+ ### 1.176.0 (2026-10-06)
306
+ * (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
+
308
+ ### 1.175.2 (2026-10-06)
309
+ * (gokturk413) feat(navigation): app bar on a phone — the centre and right slots open from a ⋮ button as a panel; the caption shortens
310
+
311
+ ### 1.175.1 (2026-10-06)
312
+ * (gokturk413) feat(controls): dashboard panel-card is a container (controls inside flow and wrap), setpoint-slider keeps room for its value bubble; side menu starts closed on phones; docs: building a dashboard that works on a phone
313
+
314
+ ### 1.175.0 (2026-10-06)
315
+ * (gokturk413) fix: a session that ran out no longer reloads the page forever ("?login&href=" loop): the session is renewed with the refresh token, else web.0's login page brings the user back to the same screen; Global → Designer holds the built-in element sets (mywebui, layout, native), Controls → Designer only the project's npm packages; screen viewer with objectType report lists the reports at once; report Generate → Signal works with local_ values and report properties (watched by the page that shows the report)
316
+
317
+ ### 1.174.0 (2026-10-02)
318
+ * (gokturk413) feat(controls): dashboard controls in global/controls/dashboard — panel-card, kpi-tile, countdown-tile, conveyor, conveyor-box, command-group, setpoint-slider (with range), logic-gate, status-chip, boiler, dial-gauge, time-spinner, notification-bell; demo screen dashboard/demo; Animations window: Start offset (0–1) so several elements on one path run evenly spaced
319
+
305
320
  ### 1.173.1 (2026-10-02)
306
321
  * (gokturk413) fix(reports): a report asked through the gateway is rendered for the gateway's user; every browser-facing relay (rest-api, webapi …) needs a proven user
307
322
 
@@ -0,0 +1,51 @@
1
+ {
2
+ "properties": {
3
+ "level": {
4
+ "type": "number",
5
+ "default": 55
6
+ },
7
+ "pv": {
8
+ "type": "number",
9
+ "default": 74
10
+ },
11
+ "sp": {
12
+ "type": "number",
13
+ "default": 75
14
+ },
15
+ "heating": {
16
+ "type": "boolean",
17
+ "default": "true"
18
+ },
19
+ "min": {
20
+ "type": "number",
21
+ "default": 0
22
+ },
23
+ "max": {
24
+ "type": "number",
25
+ "default": 100
26
+ },
27
+ "unit": {
28
+ "type": "string",
29
+ "default": "°C"
30
+ },
31
+ "decimals": {
32
+ "type": "number",
33
+ "default": 0
34
+ },
35
+ "liquidColor": {
36
+ "type": "color",
37
+ "default": "#e8453c"
38
+ },
39
+ "showValues": {
40
+ "type": "boolean",
41
+ "default": "true"
42
+ }
43
+ },
44
+ "settings": {
45
+ "width": "330px",
46
+ "height": "260px"
47
+ },
48
+ "html": "<div id=\"b\" class=\"boil\"><svg class=\"vessel\" viewBox=\"0 0 170 250\" aria-hidden=\"true\"><defs><clipPath id=\"vclip\"><rect x=\"35\" y=\"40\" width=\"100\" height=\"160\" rx=\"22\"/></clipPath><linearGradient id=\"steel\" x1=\"0\" x2=\"1\"><stop offset=\"0\" stop-color=\"#5c6573\"/><stop offset=\".45\" stop-color=\"#b9c2ce\"/><stop offset=\"1\" stop-color=\"#4a525e\"/></linearGradient><linearGradient id=\"liq\" x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\"><stop id=\"lq1\" offset=\"0\" stop-color=\"#ff6b5e\"/><stop id=\"lq2\" offset=\"1\" stop-color=\"#b3221b\"/></linearGradient></defs><rect x=\"70\" y=\"14\" width=\"30\" height=\"20\" rx=\"4\" style=\"fill:url(#steel)\"/><rect x=\"32\" y=\"32\" width=\"106\" height=\"176\" rx=\"26\" style=\"fill:url(#steel)\"/><g clip-path=\"url(#vclip)\"><rect x=\"35\" y=\"40\" width=\"100\" height=\"160\" style=\"fill:#1d2128\"/><g id=\"liquid\" transform=\"translate(0,120)\"><path class=\"wave\" d=\"M0 10 Q 25 0 50 10 T 100 10 T 150 10 T 200 10 V 220 H 0 Z\" style=\"fill:url(#liq)\"/></g><rect x=\"44\" y=\"40\" width=\"10\" height=\"160\" style=\"fill:rgba(255,255,255,.12)\"/></g><rect x=\"112\" y=\"60\" width=\"7\" height=\"120\" rx=\"3.5\" style=\"fill:#e8ecf1\"/><rect id=\"thermo\" x=\"113.5\" y=\"100\" width=\"4\" height=\"78\" rx=\"2\" style=\"fill:#ff4d4d\"/><circle cx=\"115.5\" cy=\"184\" r=\"7\" style=\"fill:#ff4d4d;stroke:#e8ecf1;stroke-width:2\"/><circle cx=\"118\" cy=\"56\" r=\"13\" style=\"fill:#e8ecf1;stroke:#7d8693;stroke-width:2\"/><line id=\"needle\" x1=\"118\" y1=\"56\" x2=\"125\" y2=\"50\" style=\"stroke:#222;stroke-width:2\"/><circle cx=\"85\" cy=\"176\" r=\"13\" style=\"fill:#e8ecf1;stroke:#7d8693;stroke-width:2\"/><line x1=\"85\" y1=\"176\" x2=\"80\" y2=\"168\" style=\"stroke:#222;stroke-width:2\"/><rect x=\"40\" y=\"206\" width=\"90\" height=\"34\" rx=\"4\" style=\"fill:#2b3038\"/><g id=\"flame\" class=\"flame\"><path d=\"M70 238 C 62 226 72 220 74 212 C 78 222 84 214 85 204 C 90 216 100 220 97 230 C 104 226 104 236 100 238 Z\" style=\"fill:#ff9a1f\"/><path d=\"M78 238 C 74 230 80 226 82 220 C 86 228 92 228 92 238 Z\" style=\"fill:#ffe066\"/></g></svg><div id=\"vals\" class=\"vals\"><span class=\"pv mono\"><span id=\"pv\"></span><small id=\"u1\"></small></span><span class=\"sp mono\"><span id=\"sp\"></span><small id=\"u2\"></small></span></div></div>",
49
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.boil{height:100%;display:flex;align-items:center;gap:12px}\n.vessel{height:100%;max-height:100%;aspect-ratio:170/250;overflow:visible;flex:none}\n.wave{animation:wave 3s ease-in-out infinite}@keyframes wave{50%{transform:translateX(-14px)}}\n.flame{transform-origin:85px 238px;animation:flick .25s ease-in-out infinite alternate}@keyframes flick{to{transform:scaleY(.82) scaleX(1.06)}}\n.vals{display:flex;flex-direction:column;gap:2px}\n.pv{font-size:36px;font-weight:600}.sp{font-size:26px;font-weight:600;color:var(--db-glow)}\n.vals small{font-size:.6em;margin-left:2px}\n.boil.novals .vals{display:none}\n",
50
+ "script": "export function levelY(level){ const l = Math.max(0, Math.min(100, Number(level) || 0)); return 40 + (1 - l / 100) * 150; }\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const yes = (v) => v === true || v === 'true' || v === 1;\n const upd = () => {\n const min = Number(instance.min) || 0, max = Number(instance.max) || 100, dec = Math.max(0, Number(instance.decimals) || 0);\n $('liquid').setAttribute('transform', 'translate(0,' + levelY(instance.level).toFixed(1) + ')');\n const pv = Number(instance.pv), f = Math.max(0, Math.min(1, ((isFinite(pv) ? pv : min) - min) / ((max - min) || 1)));\n $('thermo').setAttribute('y', String(180 - f * 110)); $('thermo').setAttribute('height', String(Math.max(2, f * 110)));\n const ang = (-140 + f * 280) * Math.PI / 180;\n $('needle').setAttribute('x2', String(118 + 9 * Math.sin(ang))); $('needle').setAttribute('y2', String(56 - 9 * Math.cos(ang)));\n $('flame').style.display = yes(instance.heating) ? '' : 'none';\n const c = instance.liquidColor || '#e8453c';\n $('lq1').setAttribute('stop-color', c); $('lq2').setAttribute('stop-color', c); $('lq2').setAttribute('stop-opacity', '.75');\n $('pv').textContent = isFinite(pv) ? pv.toFixed(dec) : '—';\n const sp = Number(instance.sp); $('sp').textContent = isFinite(sp) ? sp.toFixed(dec) : '—';\n $('u1').textContent = $('u2').textContent = instance.unit || '';\n $('b').classList.toggle('novals', instance.showValues === false || instance.showValues === 'false');\n };\n for(const p of ['level', 'pv', 'sp', 'heating', 'min', 'max', 'unit', 'decimals', 'liquid-color', 'show-values']) instance._assignEvent(p + '-changed', upd);\n upd();\n}\n"
51
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "properties": {
3
+ "signal": {
4
+ "type": "signal",
5
+ "default": ""
6
+ },
7
+ "buttons": {
8
+ "type": "string",
9
+ "default": "[{\"label\": \"ON\", \"value\": true, \"tone\": \"ok\"}, {\"label\": \"OFF\", \"value\": false, \"tone\": \"off\"}, {\"label\": \"RESET\", \"value\": true, \"tone\": \"warn\", \"momentary\": true, \"signal\": \"\"}]"
10
+ }
11
+ },
12
+ "settings": {
13
+ "width": "300px",
14
+ "height": "44px"
15
+ },
16
+ "html": "<div id=\"g\" class=\"cmdg\" role=\"group\"></div>",
17
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.cmdg{height:100%;display:inline-flex;border-radius:10px;overflow:hidden;border:1px solid var(--ui-border,#4a5262)}\nbutton{font:600 15px 'IBM Plex Sans','Segoe UI',sans-serif;letter-spacing:.04em;padding:0 22px;border:0;border-right:1px solid var(--ui-border,#4a5262);background:var(--ui-surface-2,#3a414d);color:var(--ui-dim,#9aa5b5);cursor:pointer}\nbutton:last-child{border-right:0}\nbutton.cur{background:linear-gradient(180deg,color-mix(in srgb,var(--tone) 100%,#fff 10%),color-mix(in srgb,var(--tone) 80%,#000));color:#fff}\nbutton.mom{color:var(--tone)}\nbutton.mom.flash,button.mom:active{background:linear-gradient(180deg,color-mix(in srgb,var(--tone) 100%,#fff 15%),color-mix(in srgb,var(--tone) 80%,#000));color:#1d1404}\nbutton:focus-visible{outline:2px solid var(--accent,#57b6ff);outline-offset:-3px}\n",
18
+ "script": "function dbSubscribe(instance, key, id, onVal){\n dbUnsub(instance, key);\n if(!id || !window.IOB) return;\n instance.__dbSubs = instance.__dbSubs || {};\n const cb = function(_i, s){ onVal(s ? s.val : null); };\n instance.__dbSubs[key] = [id, cb];\n try{ window.IOB.subscribeState(id, cb); }catch(e){}\n try{ window.IOB.getState(id).then(function(s){ onVal(s ? s.val : null); }).catch(function(){}); }catch(e){}\n // read quality knows this state (badge on the control when the value cannot be trusted)\n try{ instance.__dbRq = instance.__dbRq || {}; instance.__dbRq[key] = window.IOB.watchReadQuality && window.IOB.watchReadQuality(instance, id, { prop: key, twoWay: true }); }catch(e){}\n}\nfunction dbUnsub(instance, key){\n const p = instance.__dbSubs && instance.__dbSubs[key];\n if(p){ try{ window.IOB.unsubscribeState(p[0], p[1]); }catch(e){} delete instance.__dbSubs[key]; }\n const r = instance.__dbRq && instance.__dbRq[key];\n if(typeof r === 'function'){ try{ r(); }catch(e){} }\n if(instance.__dbRq) delete instance.__dbRq[key];\n}\nfunction dbUnsubAll(instance){ for(const k of Object.keys(instance.__dbSubs || {})) dbUnsub(instance, k); }\n// a write through IOB.setState: Confirm & Protect may ask first; cancelled = nothing written\nfunction dbWrite(instance, id, val, onCancel){\n if(!id || !window.IOB) return Promise.resolve(false);\n return Promise.resolve(window.IOB.setState(id, val, false, { feedback: instance })).then(function(){ return true; }, function(e){\n if(e && e.cancelled){ if(onCancel) onCancel(); }\n else console.warn('[dashboard] write failed', id, e);\n return false;\n });\n}\nfunction dbBool(v){ return v === true || v === 1 || v === 'true' || v === '1' || v === 'on'; }\nfunction dbNum(v, d){ const n = Number(v); return isFinite(n) ? n : d; }\nexport function parseButtons(text){\n try{ const a = JSON.parse(text || '[]'); return Array.isArray(a) ? a.filter(b => b && b.label != null) : []; }catch(e){ return []; }\n}\nexport function sameValue(a, b){\n if(a === b) return true;\n if(a == null || b == null) return false;\n if(typeof a === 'boolean' || typeof b === 'boolean') return dbBool(a) === dbBool(b);\n return String(a) === String(b);\n}\nexport function init(instance){\n const g = instance._getDomElement('g');\n let current;\n const paint = () => {\n for(const b of g.children){ const d = b.__def; b.classList.toggle('cur', !d.momentary && sameValue(current, d.value)); b.setAttribute('aria-pressed', String(b.classList.contains('cur'))); }\n };\n const build = () => {\n g.textContent = '';\n for(const d of parseButtons(instance.buttons)){\n const b = document.createElement('button');\n b.type = 'button'; b.textContent = d.label; b.__def = d;\n b.className = 't-' + (d.tone || 'accent') + (d.momentary ? ' mom' : '');\n b.addEventListener('click', async () => {\n const id = d.signal || instance.signal;\n if(d.momentary){\n b.classList.add('flash'); setTimeout(() => b.classList.remove('flash'), 400);\n // a pulse: the value, then back (true → false, 1 → 0)\n if(await dbWrite(instance, id, d.value)) setTimeout(() => dbWrite(instance, id, typeof d.value === 'boolean' ? !d.value : 0), 500);\n } else dbWrite(instance, id, d.value);\n });\n g.appendChild(b);\n }\n paint();\n };\n const sub = () => dbSubscribe(instance, 'signal', instance.signal, (v) => { current = v; paint(); });\n instance._assignEvent('buttons-changed', build);\n instance._assignEvent('signal-changed', sub);\n build(); sub();\n}\nexport function disconnectedCallback(instance){ dbUnsubAll(instance); }\n"
19
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "properties": {
3
+ "color": {
4
+ "type": "color",
5
+ "default": "#d9b37c"
6
+ },
7
+ "label": {
8
+ "type": "string",
9
+ "default": ""
10
+ }
11
+ },
12
+ "settings": {
13
+ "width": "58px",
14
+ "height": "40px"
15
+ },
16
+ "html": "<div id=\"b\" class=\"box\"><span id=\"lbl\"></span></div>",
17
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.box{height:100%;border-radius:5px;position:relative;display:grid;place-items:center;font:600 10px 'IBM Plex Mono',monospace;color:rgba(0,0,0,.55);\n background:linear-gradient(180deg,color-mix(in srgb,var(--c) 100%,#fff 12%),color-mix(in srgb,var(--c) 85%,#000));box-shadow:inset 0 0 0 1px rgba(0,0,0,.18),0 4px 8px rgba(0,0,0,.25)}\n.box::after{content:\"\";position:absolute;left:46%;top:0;bottom:0;width:8%;background:rgba(0,0,0,.1)}\n",
18
+ "script": "export function init(instance){\n const upd = () => { const b = instance._getDomElement('b'); b.style.setProperty('--c', instance.color || '#d9b37c'); instance._getDomElement('lbl').textContent = instance.label || ''; };\n instance._assignEvent('color-changed', upd); instance._assignEvent('label-changed', upd);\n upd();\n}\n"
19
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "properties": {
3
+ "running": {
4
+ "type": "boolean",
5
+ "default": "true"
6
+ },
7
+ "speed": {
8
+ "type": "number",
9
+ "default": 50
10
+ },
11
+ "beacon": {
12
+ "type": "boolean",
13
+ "default": "true"
14
+ },
15
+ "beaconColor": {
16
+ "type": "color",
17
+ "default": "#ffc93a"
18
+ },
19
+ "direction": {
20
+ "type": "enum",
21
+ "values": [
22
+ "right",
23
+ "left"
24
+ ],
25
+ "default": "right"
26
+ }
27
+ },
28
+ "settings": {
29
+ "width": "560px",
30
+ "height": "80px"
31
+ },
32
+ "html": "<div id=\"c\" class=\"conv\"><div class=\"motor\"></div><div class=\"belt\"><div id=\"surf\" class=\"surf\"></div></div><span class=\"roll\" style=\"left:34px\"></span><span class=\"roll\" style=\"left:24%\"></span><span class=\"roll\" style=\"left:44%\"></span><span class=\"roll\" style=\"left:64%\"></span><span class=\"roll\" style=\"left:84%\"></span><span id=\"beacon\" class=\"beacon\"></span></div>",
33
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.conv{position:relative;height:100%;min-height:60px}\n.motor{position:absolute;left:0;bottom:10%;width:30px;height:62%;border-radius:6px;background:linear-gradient(90deg,#3a404a,#5e6672,#3a404a)}\n.belt{position:absolute;left:18px;right:40px;bottom:12%;height:44%;border-radius:999px;background:linear-gradient(180deg,#4b5361,#2a2f38);box-shadow:inset 0 2px 0 rgba(255,255,255,.08),0 6px 14px rgba(0,0,0,.35);overflow:hidden}\n.surf{position:absolute;inset:18% 18px;border-radius:10px;background:repeating-linear-gradient(90deg,#20252d 0 10px,#262c35 10px 20px);animation:belt var(--dur,1.6s) linear infinite;animation-play-state:var(--run,running)}\n.conv.left .surf{animation-direction:reverse}\n@keyframes belt{to{background-position:20px 0}}\n.roll{position:absolute;bottom:calc(12% + 7px);width:18px;height:18px;border-radius:50%;background:radial-gradient(circle at 40% 35%,#c9d1dc,#6d7684 60%,#3d434d);border:1px solid #2a2f37}\n.beacon{position:absolute;right:16px;bottom:58%;width:18px;height:26px;border-radius:9px 9px 3px 3px;background:#7a5a10}\n.beacon.on{background:var(--bc);box-shadow:0 0 14px var(--bc),0 0 34px color-mix(in srgb,var(--bc) 50%,transparent);animation:blink 1s steps(2) infinite}\n@keyframes blink{50%{opacity:.45}}\n",
34
+ "script": "export function beltSeconds(speed){ const s = Math.max(1, Math.min(100, Number(speed) || 0)); return 1.6 * 50 / s; }\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const yes = (v) => v === true || v === 'true';\n const upd = () => {\n const c = $('c');\n const run = yes(instance.running) && Number(instance.speed) > 0;\n c.style.setProperty('--run', run ? 'running' : 'paused');\n c.style.setProperty('--dur', beltSeconds(instance.speed).toFixed(2) + 's');\n c.classList.toggle('left', instance.direction === 'left');\n const b = $('beacon');\n b.style.setProperty('--bc', instance.beaconColor || '#ffc93a');\n b.classList.toggle('on', yes(instance.beacon));\n };\n for(const p of ['running', 'speed', 'beacon', 'beacon-color', 'direction']) instance._assignEvent(p + '-changed', upd);\n upd();\n}\n"
35
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "properties": {
3
+ "label": {
4
+ "type": "string",
5
+ "default": "Remaining Time"
6
+ },
7
+ "seconds": {
8
+ "type": "number",
9
+ "default": 38
10
+ },
11
+ "endTime": {
12
+ "type": "number",
13
+ "default": 0
14
+ },
15
+ "format": {
16
+ "type": "enum",
17
+ "values": [
18
+ "auto",
19
+ "mm:ss",
20
+ "hh:mm:ss"
21
+ ],
22
+ "default": "auto"
23
+ }
24
+ },
25
+ "settings": {
26
+ "width": "200px",
27
+ "height": "72px"
28
+ },
29
+ "html": "<div class=\"kpi\"><span id=\"lbl\" class=\"lbl\"></span><span id=\"val\" class=\"val mono\"></span></div>",
30
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.kpi{height:100%;display:flex;flex-direction:column;justify-content:center;gap:2px;padding:10px 14px;border-radius:12px;background:var(--ui-surface-2,#3a414d);border:1px solid var(--ui-border,#4a5262)}\n.lbl{font-size:13px;color:var(--ui-dim,#9aa5b5)}.val{font-size:28px;font-weight:500}\n",
31
+ "script": "export function fmtDuration(sec, format){\n sec = Math.max(0, Math.floor(Number(sec) || 0));\n const h = Math.floor(sec / 3600), m = Math.floor(sec % 3600 / 60), s = sec % 60;\n const p = (n) => String(n).padStart(2, '0');\n if(format === 'hh:mm:ss' || (format !== 'mm:ss' && h > 0)) return p(h) + ':' + p(m) + ':' + p(s);\n return p(h * 60 + m) + ':' + p(s);\n}\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const left = () => {\n const end = Number(instance.endTime) || 0;\n return end > 0 ? (end - Date.now()) / 1000 : Number(instance.seconds) || 0;\n };\n const upd = () => { $('lbl').textContent = instance.label || ''; $('val').textContent = fmtDuration(left(), instance.format); };\n for(const p of ['label', 'seconds', 'end-time', 'format']) instance._assignEvent(p + '-changed', upd);\n clearInterval(instance.__cdTimer);\n // with an end time the tile counts down by itself\n instance.__cdTimer = setInterval(() => { if(Number(instance.endTime) > 0) upd(); }, 1000);\n upd();\n}\nexport function disconnectedCallback(instance){ clearInterval(instance.__cdTimer); }\n"
32
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "properties": {
3
+ "value": {
4
+ "type": "number",
5
+ "default": 3.2
6
+ },
7
+ "min": {
8
+ "type": "number",
9
+ "default": 0
10
+ },
11
+ "max": {
12
+ "type": "number",
13
+ "default": 6
14
+ },
15
+ "unit": {
16
+ "type": "string",
17
+ "default": "bar"
18
+ },
19
+ "label": {
20
+ "type": "string",
21
+ "default": ""
22
+ },
23
+ "decimals": {
24
+ "type": "number",
25
+ "default": 1
26
+ },
27
+ "zones": {
28
+ "type": "string",
29
+ "default": "0-4:ok,4-5:warn,5-6:error"
30
+ }
31
+ },
32
+ "settings": {
33
+ "width": "120px",
34
+ "height": "120px"
35
+ },
36
+ "html": "<svg id=\"s\" class=\"dial\" viewBox=\"0 0 120 120\"><circle cx=\"60\" cy=\"60\" r=\"56\" class=\"face\"/><g id=\"zones\"></g><g id=\"ticks\"></g><line id=\"nd\" x1=\"60\" y1=\"60\" x2=\"60\" y2=\"20\" class=\"nd\"/><circle cx=\"60\" cy=\"60\" r=\"5\" class=\"hub\"/><text id=\"v\" x=\"60\" y=\"88\" class=\"vt\">0</text><text id=\"u\" x=\"60\" y=\"101\" class=\"ut\"></text><text id=\"l\" x=\"60\" y=\"74\" class=\"lt\"></text></svg>",
37
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.dial{width:100%;height:100%;display:block}\n.face{fill:var(--ui-surface-2,#3a414d);stroke:var(--ui-border,#4a5262);stroke-width:2}\n.tk{stroke:var(--ui-dim,#9aa5b5);stroke-width:1.5}\n.nd{stroke:var(--ui-error,#ff5a6a);stroke-width:2.5;stroke-linecap:round;transition:transform .4s;transform-origin:60px 60px}\n.hub{fill:var(--ui-text,#e6ebf2)}\n.vt{font:600 15px 'IBM Plex Mono',monospace;fill:var(--ui-text,#e6ebf2);text-anchor:middle}\n.ut{font:11px 'IBM Plex Sans',sans-serif;fill:var(--ui-dim,#9aa5b5);text-anchor:middle}\n.lt{font:9px 'IBM Plex Sans',sans-serif;fill:var(--ui-dim,#9aa5b5);text-anchor:middle;letter-spacing:.04em}\n.z{fill:none;stroke-width:6}\n.z.ok{stroke:var(--ui-ok,#3ccf7a)}.z.warn{stroke:var(--ui-warn,#f2a531)}.z.error{stroke:var(--ui-error,#ff5a6a)}.z.info{stroke:var(--ui-info,#57b6ff)}\n",
38
+ "script": "export function parseZones(text){\n return String(text || '').split(',').map(s => s.trim()).filter(Boolean).map(s => {\n const m = /^(-?[\\d.]+)\\s*-\\s*(-?[\\d.]+)\\s*:\\s*(\\w+)$/.exec(s);\n return m ? { from: Number(m[1]), to: Number(m[2]), tone: m[3] } : null;\n }).filter(Boolean);\n}\nconst A0 = -135, SPAN = 270;\nfunction pt(a, r){ const t = (a - 90) * Math.PI / 180; return [60 + r * Math.cos(t), 60 + r * Math.sin(t)]; }\nfunction arc(a1, a2, r){ const [x1, y1] = pt(a1, r), [x2, y2] = pt(a2, r); return 'M' + x1.toFixed(2) + ' ' + y1.toFixed(2) + ' A' + r + ' ' + r + ' 0 ' + (a2 - a1 > 180 ? 1 : 0) + ' 1 ' + x2.toFixed(2) + ' ' + y2.toFixed(2); }\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const NS = 'http://www.w3.org/2000/svg';\n const upd = () => {\n const min = Number(instance.min) || 0, max = Number(instance.max) || 100, span = (max - min) || 1;\n const ang = (v) => A0 + Math.max(0, Math.min(1, (v - min) / span)) * SPAN;\n const z = $('zones'); z.textContent = '';\n for(const zn of parseZones(instance.zones)){ const p = document.createElementNS(NS, 'path'); p.setAttribute('d', arc(ang(zn.from), ang(zn.to), 48)); p.setAttribute('class', 'z ' + zn.tone); z.appendChild(p); }\n const t = $('ticks'); t.textContent = '';\n for(let i = 0; i <= 10; i++){ const a = A0 + i * SPAN / 10, [x1, y1] = pt(a, 41), [x2, y2] = pt(a, i % 5 ? 37 : 34); const l = document.createElementNS(NS, 'line'); l.setAttribute('x1', x1); l.setAttribute('y1', y1); l.setAttribute('x2', x2); l.setAttribute('y2', y2); l.setAttribute('class', 'tk'); t.appendChild(l); }\n const v = Number(instance.value);\n $('nd').style.transform = 'rotate(' + ang(isFinite(v) ? v : min) + 'deg)';\n $('v').textContent = isFinite(v) ? v.toFixed(Math.max(0, Number(instance.decimals) || 0)) : '—';\n $('u').textContent = instance.unit || ''; $('l').textContent = instance.label || '';\n };\n for(const p of ['value', 'min', 'max', 'unit', 'label', 'decimals', 'zones']) instance._assignEvent(p + '-changed', upd);\n upd();\n}\n"
39
+ }
@@ -0,0 +1,52 @@
1
+ {
2
+ "properties": {
3
+ "label": {
4
+ "type": "string",
5
+ "default": "Accumulated Items"
6
+ },
7
+ "value": {
8
+ "type": "string",
9
+ "default": "1264"
10
+ },
11
+ "unit": {
12
+ "type": "string",
13
+ "default": ""
14
+ },
15
+ "decimals": {
16
+ "type": "number",
17
+ "default": -1
18
+ },
19
+ "alarmCount": {
20
+ "type": "number",
21
+ "default": 0
22
+ },
23
+ "ringMax": {
24
+ "type": "number",
25
+ "default": 0
26
+ },
27
+ "tone": {
28
+ "type": "enum",
29
+ "values": [
30
+ "ok",
31
+ "warn",
32
+ "error",
33
+ "info",
34
+ "accent",
35
+ "glow",
36
+ "off"
37
+ ],
38
+ "default": "warn"
39
+ },
40
+ "frame": {
41
+ "type": "boolean",
42
+ "default": "true"
43
+ }
44
+ },
45
+ "settings": {
46
+ "width": "330px",
47
+ "height": "72px"
48
+ },
49
+ "html": "<div id=\"k\" class=\"kpi\"><svg id=\"ring\" class=\"ring\" viewBox=\"0 0 40 40\" aria-hidden=\"true\"><circle cx=\"20\" cy=\"20\" r=\"15\" class=\"rt\"/><circle id=\"rv\" cx=\"20\" cy=\"20\" r=\"15\" class=\"rv\" transform=\"rotate(-90 20 20)\"/></svg><span id=\"lbl\" class=\"lbl\"></span><span class=\"val mono\"><span id=\"val\"></span><small id=\"unit\"></small></span><span id=\"al\" class=\"al\">🔔<span id=\"aln\"></span></span></div>",
50
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.kpi{height:100%;display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:12px;background:var(--ui-surface-2,#3a414d);border:1px solid var(--ui-border,#4a5262);min-width:0}\n.kpi.bare{background:none;border:0;padding:0}\n.ring{width:38px;height:38px;flex:none}.rt{fill:none;stroke:var(--ui-border,#4a5262);stroke-width:4}\n.rv{fill:none;stroke:var(--tone);stroke-width:4;stroke-linecap:round;stroke-dasharray:94.25;transition:stroke-dashoffset .4s}\n.lbl{font-size:13px;color:var(--ui-dim,#9aa5b5);line-height:1.3;white-space:pre-line}\n.val{margin-left:auto;font-size:30px;font-weight:500;white-space:nowrap}.val small{font-size:14px;color:var(--ui-dim,#9aa5b5);margin-left:4px}\n.al{display:flex;align-items:center;gap:3px;color:var(--ui-warn,#f2a531);font:600 13px 'IBM Plex Mono',monospace}\n",
51
+ "script": "export function fmtValue(v, decimals){\n const n = Number(v);\n if(v === null || v === undefined || v === '') return '—';\n if(!isFinite(n) || typeof v === 'boolean') return String(v);\n const d = Number(decimals);\n return d >= 0 ? n.toFixed(d) : String(Math.round(n * 1000) / 1000);\n}\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const upd = () => {\n $('k').className = 'kpi t-' + (instance.tone || 'warn') + ((instance.frame === false || instance.frame === 'false') ? ' bare' : '');\n $('lbl').textContent = instance.label || '';\n $('val').textContent = fmtValue(instance.value, instance.decimals);\n $('unit').textContent = instance.unit || '';\n const al = Number(instance.alarmCount) || 0;\n $('al').style.display = al > 0 ? '' : 'none';\n $('aln').textContent = String(al);\n const max = Number(instance.ringMax) || 0;\n $('ring').style.display = max > 0 ? '' : 'none';\n if(max > 0){ const p = Math.max(0, Math.min(1, (Number(instance.value) || 0) % (max + 1e-9) / max)); $('rv').style.strokeDashoffset = String(94.25 * (1 - p)); }\n };\n for(const p of ['label', 'value', 'unit', 'decimals', 'alarm-count', 'ring-max', 'tone', 'frame']) instance._assignEvent(p + '-changed', upd);\n upd();\n}\n"
52
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "properties": {
3
+ "gate": {
4
+ "type": "enum",
5
+ "values": [
6
+ "AND",
7
+ "OR",
8
+ "XOR",
9
+ "NAND",
10
+ "NOR",
11
+ "NOT",
12
+ "ARROW"
13
+ ],
14
+ "default": "AND"
15
+ },
16
+ "in1": {
17
+ "type": "boolean",
18
+ "default": "false"
19
+ },
20
+ "in2": {
21
+ "type": "boolean",
22
+ "default": "false"
23
+ },
24
+ "out": {
25
+ "type": "string",
26
+ "default": ""
27
+ },
28
+ "label": {
29
+ "type": "string",
30
+ "default": ""
31
+ }
32
+ },
33
+ "settings": {
34
+ "width": "150px",
35
+ "height": "50px"
36
+ },
37
+ "html": "<div id=\"g\" class=\"lg\"><i id=\"i1\" class=\"pin p1\"></i><i id=\"i2\" class=\"pin p2\"></i><span class=\"w w1\"></span><span class=\"w w2\"></span><span id=\"body\" class=\"gate\"></span><span class=\"w wo\"></span><i id=\"o\" class=\"pin po\"></i></div>",
38
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.lg{position:relative;height:100%}\n.gate{position:absolute;left:24px;right:24px;top:50%;height:34px;transform:translateY(-50%);border-radius:17px;display:grid;place-items:center;font:700 14px 'IBM Plex Sans',sans-serif;letter-spacing:.04em;\n background:linear-gradient(180deg,#f2f4f7,#c9cfd8);color:#2a313b;box-shadow:0 3px 8px rgba(0,0,0,.3)}\n.lg.on .gate{box-shadow:0 0 0 2px var(--db-glow),0 0 16px var(--db-glow)}\n.w{position:absolute;height:2px;background:var(--ui-border,#4a5262)}\n.w1{left:8px;width:16px;top:calc(50% - 9px)}.w2{left:8px;width:16px;top:calc(50% + 7px)}.wo{right:8px;width:16px;top:calc(50% - 1px)}\n.w.on{background:var(--db-glow);box-shadow:0 0 6px var(--db-glow)}\n.pin{position:absolute;width:9px;height:9px;border-radius:50%;border:2px solid var(--ui-dim,#9aa5b5);background:var(--ui-surface,#323843)}\n.p1{left:0;top:calc(50% - 13px)}.p2{left:0;top:calc(50% + 3px)}.po{right:0;top:calc(50% - 5px)}\n.pin.on{background:var(--db-glow);border-color:var(--db-glow);box-shadow:0 0 8px var(--db-glow)}\n.lg.one .p2,.lg.one .w2{display:none}.lg.one .w1{top:calc(50% - 1px)}.lg.one .p1{top:calc(50% - 5px)}\n",
39
+ "script": "export function evalGate(gate, a, b){\n switch(gate){\n case 'AND': return a && b;\n case 'OR': return a || b;\n case 'XOR': return a !== b;\n case 'NAND': return !(a && b);\n case 'NOR': return !(a || b);\n case 'NOT': return !a;\n default: return a; // ARROW passes its input on\n }\n}\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const yes = (v) => v === true || v === 1 || v === 'true' || v === '1';\n const upd = () => {\n const gate = instance.gate || 'AND', one = gate === 'NOT' || gate === 'ARROW';\n const a = yes(instance.in1), b = yes(instance.in2);\n const out = instance.out === '' || instance.out == null ? evalGate(gate, a, b) : yes(instance.out);\n $('body').textContent = instance.label || (gate === 'ARROW' ? '⇒' : gate);\n const g = $('g');\n g.classList.toggle('one', one); g.classList.toggle('on', out);\n $('i1').classList.toggle('on', a); $('i2').classList.toggle('on', b); $('o').classList.toggle('on', out);\n const w = g.querySelectorAll('.w'); w[0].classList.toggle('on', a); w[1].classList.toggle('on', b); w[2].classList.toggle('on', out);\n if(instance.__out !== out){ instance.__out = out; instance.dispatchEvent(new CustomEvent('output-changed', { detail: { value: out }, bubbles: true, composed: true })); }\n };\n for(const p of ['gate', 'in1', 'in2', 'out', 'label']) instance._assignEvent(p + '-changed', upd);\n upd();\n}\n"
40
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "properties": {
3
+ "count": {
4
+ "type": "number",
5
+ "default": 4
6
+ },
7
+ "screen": {
8
+ "type": "screen",
9
+ "default": ""
10
+ }
11
+ },
12
+ "settings": {
13
+ "width": "40px",
14
+ "height": "40px"
15
+ },
16
+ "html": "<button id=\"b\" class=\"ib\" type=\"button\" aria-label=\"Notifications\"><svg class=\"bi\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M12 22a2.5 2.5 0 0 0 2.4-2h-4.8A2.5 2.5 0 0 0 12 22zm7-6V11a7 7 0 0 0-5.5-6.8V3.5a1.5 1.5 0 0 0-3 0v.7A7 7 0 0 0 5 11v5l-2 2v1h18v-1z\"/></svg><span id=\"n\" class=\"bdg mono\"></span></button>",
17
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.ib{width:100%;height:100%;border-radius:9px;display:grid;place-items:center;background:transparent;border:0;color:var(--ui-dim,#9aa5b5);cursor:pointer;position:relative}\n.ib:hover{color:var(--ui-text,#e6ebf2)}.bi{width:60%;height:60%}\n.bdg{position:absolute;top:2px;right:2px;min-width:16px;height:16px;padding:0 4px;border-radius:8px;background:var(--ui-error,#ff5a6a);color:#fff;font-size:10px;font-weight:700;display:grid;place-items:center}\n.ib:focus-visible{outline:2px solid var(--accent,#57b6ff)}\n",
18
+ "script": "export function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const upd = () => { const n = Number(instance.count) || 0; $('n').textContent = n > 99 ? '99+' : String(n); $('n').style.display = n > 0 ? '' : 'none'; };\n $('b').addEventListener('click', () => {\n if(instance.screen && window.IOB && window.IOB.changeView) window.IOB.changeView.emit(instance.screen);\n instance.dispatchEvent(new CustomEvent('bellclick', { bubbles: true, composed: true }));\n });\n instance._assignEvent('count-changed', upd);\n upd();\n}\n"
19
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "properties": {
3
+ "title": {
4
+ "type": "string",
5
+ "default": "Panel"
6
+ },
7
+ "activeDot": {
8
+ "type": "number",
9
+ "default": 1
10
+ },
11
+ "showMenu": {
12
+ "type": "boolean",
13
+ "default": "true"
14
+ },
15
+ "direction": {
16
+ "type": "enum",
17
+ "values": [
18
+ "row",
19
+ "column"
20
+ ],
21
+ "default": "row"
22
+ },
23
+ "gap": {
24
+ "type": "string",
25
+ "default": "12px"
26
+ },
27
+ "align": {
28
+ "type": "enum",
29
+ "values": [
30
+ "stretch",
31
+ "flex-start",
32
+ "center",
33
+ "flex-end"
34
+ ],
35
+ "default": "center"
36
+ }
37
+ },
38
+ "settings": {
39
+ "width": "600px",
40
+ "height": "360px"
41
+ },
42
+ "html": "<div class=\"ch\"><span id=\"ttl\"></span><span id=\"dots\" class=\"dots\" title=\"Menu\"><i></i><i></i><i></i></span></div><slot></slot>",
43
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n:host{display:flex;flex-wrap:wrap;align-content:flex-start;align-items:center;gap:12px;padding:0 16px 16px;position:relative;min-width:0;\n border-radius:14px;border:1px solid var(--ui-border,#4a5262);\n background:linear-gradient(180deg,color-mix(in srgb,var(--ui-surface,#323843) 92%,#fff 3%),var(--ui-surface,#323843));box-shadow:0 10px 30px rgba(0,0,0,.25)}\n.ch{flex:0 0 calc(100% + 32px);align-self:stretch;margin:0 -16px 4px;display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--ui-border,#4a5262);font-weight:500;font-size:16px}\n.dots{display:flex;gap:5px;cursor:pointer;padding:4px}.dots i{width:6px;height:6px;border-radius:50%;background:var(--ui-dim,#9aa5b5);opacity:.6}\n.dots i.on{background:var(--db-glow);opacity:1;box-shadow:0 0 6px var(--db-glow)}\n::slotted(*){max-width:100%;min-width:0}\n",
44
+ "script": "export function init(instance){\n const upd = () => {\n instance._getDomElement('ttl').textContent = instance.title || '';\n const dots = instance._getDomElement('dots');\n dots.style.display = (instance.showMenu === false || instance.showMenu === 'false') ? 'none' : '';\n const a = Number(instance.activeDot) || 0;\n [...dots.children].forEach((d, i) => d.classList.toggle('on', i + 1 === a));\n const col = instance.direction === 'column';\n instance.style.flexDirection = col ? 'column' : 'row';\n instance.style.flexWrap = col ? 'nowrap' : 'wrap';\n instance.style.gap = instance.gap || '12px';\n instance.style.alignItems = col ? 'stretch' : (instance.align || 'center');\n };\n instance._getDomElement('dots').addEventListener('click', () =>\n instance.dispatchEvent(new CustomEvent('menu', { bubbles: true, composed: true })));\n for(const p of ['title', 'active-dot', 'show-menu', 'direction', 'gap', 'align']) instance._assignEvent(p + '-changed', upd);\n upd();\n // at runtime the controls inside flow: a position left over from the designer's free\n // placement is dropped (the designer keeps it while editing)\n if(!document.querySelector('iobroker-webui-app-shell')){\n const flow = () => { for(const c of instance.children) if(c.style && c.style.position === 'absolute'){ c.style.position = ''; c.style.left = ''; c.style.top = ''; } };\n flow();\n instance.__pcMo = new MutationObserver(flow);\n instance.__pcMo.observe(instance, { childList: true });\n }\n}\nexport function disconnectedCallback(instance){ try{ instance.__pcMo && instance.__pcMo.disconnect(); }catch(e){} }\n"
45
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "properties": {
3
+ "label": {
4
+ "type": "string",
5
+ "default": "Main motor"
6
+ },
7
+ "signal": {
8
+ "type": "signal",
9
+ "default": ""
10
+ },
11
+ "signalHigh": {
12
+ "type": "signal",
13
+ "default": ""
14
+ },
15
+ "min": {
16
+ "type": "number",
17
+ "default": 1000
18
+ },
19
+ "max": {
20
+ "type": "number",
21
+ "default": 1400
22
+ },
23
+ "step": {
24
+ "type": "number",
25
+ "default": 10
26
+ },
27
+ "unit": {
28
+ "type": "string",
29
+ "default": ""
30
+ },
31
+ "decimals": {
32
+ "type": "number",
33
+ "default": 0
34
+ },
35
+ "showLabel": {
36
+ "type": "boolean",
37
+ "default": "true"
38
+ }
39
+ },
40
+ "settings": {
41
+ "width": "520px",
42
+ "height": "86px"
43
+ },
44
+ "html": "<div class=\"sld\"><span id=\"nm\" class=\"nm\"></span><div id=\"tr\" class=\"track\"><div class=\"ticks\"></div><div id=\"fill\" class=\"fill\"></div><div id=\"t1\" class=\"thumb\" tabindex=\"0\" role=\"slider\"><span class=\"bub mono\"></span></div><div id=\"t2\" class=\"thumb\" tabindex=\"0\" role=\"slider\"><span class=\"bub mono\"></span></div></div><div class=\"mm mono\"><span id=\"mn\"></span><span id=\"mx\"></span></div></div>",
45
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.sld{height:100%;display:grid;grid-template-columns:auto minmax(0,1fr);gap:2px 12px;align-items:center;padding-top:22px;box-sizing:border-box}\n.sld.nolabel{grid-template-columns:minmax(0,1fr)}.sld.nolabel .nm{display:none}.sld.nolabel .mm{grid-column:1}\n.nm{font-size:14px;color:var(--ui-dim,#9aa5b5)}\n.track{position:relative;height:30px;margin:0 11px}\n.ticks{position:absolute;left:0;right:0;top:4px;height:16px;background:repeating-linear-gradient(90deg,var(--ui-border,#4a5262) 0 1px,transparent 1px 8px)}\n.ticks::after{content:\"\";position:absolute;inset:0;background:repeating-linear-gradient(90deg,var(--ui-dim,#9aa5b5) 0 1px,transparent 1px 40px);opacity:.6}\n.fill{position:absolute;top:11px;height:3px;border-radius:2px;background:var(--accent,#57b6ff);box-shadow:0 0 8px var(--accent,#57b6ff)}\n.thumb{position:absolute;top:2px;width:22px;height:22px;margin-left:-11px;border-radius:50%;cursor:grab;touch-action:none;\n background:radial-gradient(circle,#fff 30%,color-mix(in srgb,var(--accent,#57b6ff) 60%,#fff));border:3px solid var(--accent,#57b6ff);box-shadow:0 0 12px var(--accent,#57b6ff)}\n.thumb:focus-visible{outline:2px solid var(--ui-text,#fff);outline-offset:2px}\n.bub{position:absolute;bottom:26px;left:50%;transform:translateX(-50%);font-size:11px;font-weight:600;background:var(--ui-surface-2,#3a414d);border:1px solid var(--ui-border,#4a5262);border-radius:5px;padding:1px 5px;white-space:nowrap}\n.mm{grid-column:2;display:flex;justify-content:space-between;font-size:12px;color:var(--ui-dim,#9aa5b5);margin:0 4px}\n",
46
+ "script": "function dbSubscribe(instance, key, id, onVal){\n dbUnsub(instance, key);\n if(!id || !window.IOB) return;\n instance.__dbSubs = instance.__dbSubs || {};\n const cb = function(_i, s){ onVal(s ? s.val : null); };\n instance.__dbSubs[key] = [id, cb];\n try{ window.IOB.subscribeState(id, cb); }catch(e){}\n try{ window.IOB.getState(id).then(function(s){ onVal(s ? s.val : null); }).catch(function(){}); }catch(e){}\n // read quality knows this state (badge on the control when the value cannot be trusted)\n try{ instance.__dbRq = instance.__dbRq || {}; instance.__dbRq[key] = window.IOB.watchReadQuality && window.IOB.watchReadQuality(instance, id, { prop: key, twoWay: true }); }catch(e){}\n}\nfunction dbUnsub(instance, key){\n const p = instance.__dbSubs && instance.__dbSubs[key];\n if(p){ try{ window.IOB.unsubscribeState(p[0], p[1]); }catch(e){} delete instance.__dbSubs[key]; }\n const r = instance.__dbRq && instance.__dbRq[key];\n if(typeof r === 'function'){ try{ r(); }catch(e){} }\n if(instance.__dbRq) delete instance.__dbRq[key];\n}\nfunction dbUnsubAll(instance){ for(const k of Object.keys(instance.__dbSubs || {})) dbUnsub(instance, k); }\n// a write through IOB.setState: Confirm & Protect may ask first; cancelled = nothing written\nfunction dbWrite(instance, id, val, onCancel){\n if(!id || !window.IOB) return Promise.resolve(false);\n return Promise.resolve(window.IOB.setState(id, val, false, { feedback: instance })).then(function(){ return true; }, function(e){\n if(e && e.cancelled){ if(onCancel) onCancel(); }\n else console.warn('[dashboard] write failed', id, e);\n return false;\n });\n}\nfunction dbBool(v){ return v === true || v === 1 || v === 'true' || v === '1' || v === 'on'; }\nfunction dbNum(v, d){ const n = Number(v); return isFinite(n) ? n : d; }\nexport function snap(v, min, max, step){\n v = Math.min(max, Math.max(min, Number(v)));\n if(step > 0) v = min + Math.round((v - min) / step) * step;\n return Math.round(Math.min(max, Math.max(min, v)) * 1e6) / 1e6;\n}\n/** the two thumbs never cross: the moved one stops one step before the other */\nexport function orderRange(lo, hi, moved, step){\n const gap = step > 0 ? step : 0;\n if(moved === 'lo') return [Math.min(lo, hi - gap), hi];\n return [lo, Math.max(hi, lo + gap)];\n}\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n const st = { lo: null, hi: null };\n const range = () => !!instance.signalHigh;\n const P = () => ({ min: dbNum(instance.min, 0), max: dbNum(instance.max, 100), step: dbNum(instance.step, 1) });\n const draw = () => {\n const { min, max } = P(), dec = Math.max(0, dbNum(instance.decimals, 0));\n const pct = (v) => ((dbNum(v, min) - min) / ((max - min) || 1)) * 100;\n $('nm').textContent = instance.label || '';\n $('nm').parentElement.classList.toggle('nolabel', instance.showLabel === false || instance.showLabel === 'false');\n $('mn').textContent = String(min); $('mx').textContent = String(max);\n const lo = st.lo ?? min, hi = st.hi ?? max;\n for(const [t, v] of [[$('t1'), lo], [$('t2'), hi]]){\n t.style.left = pct(v) + '%';\n t.querySelector('.bub').textContent = dbNum(v, min).toFixed(dec) + (instance.unit ? ' ' + instance.unit : '');\n t.setAttribute('aria-valuenow', String(v)); t.setAttribute('aria-valuemin', String(min)); t.setAttribute('aria-valuemax', String(max));\n }\n $('t2').style.display = range() ? '' : 'none';\n const f = $('fill');\n if(range()){ f.style.left = pct(lo) + '%'; f.style.width = Math.max(0, pct(hi) - pct(lo)) + '%'; }\n else { f.style.left = '0'; f.style.width = pct(lo) + '%'; }\n };\n const idOf = (k) => k === 'lo' ? instance.signal : instance.signalHigh;\n const refresh = (k) => { const id = idOf(k); if(id) window.IOB.getState(id).then(s => { st[k] = s ? dbNum(s.val, null) : null; draw(); }).catch(() => {}); };\n const setV = (k, v) => {\n const { min, max, step } = P();\n st[k] = snap(v, min, max, step);\n if(range()){ const [a, b] = orderRange(st.lo ?? min, st.hi ?? max, k, step); st.lo = a; st.hi = b; }\n draw();\n };\n const commit = (k) => dbWrite(instance, idOf(k), st[k], () => refresh(k));\n for(const [el, k] of [[$('t1'), 'lo'], [$('t2'), 'hi']]){\n el.addEventListener('pointerdown', (e) => {\n el.setPointerCapture(e.pointerId);\n const { min, max } = P();\n const mv = (ev) => { const r = $('tr').getBoundingClientRect(); setV(k, min + (ev.clientX - r.left) / (r.width || 1) * (max - min)); };\n const up = () => { el.removeEventListener('pointermove', mv); el.removeEventListener('pointerup', up); commit(k); };\n el.addEventListener('pointermove', mv); el.addEventListener('pointerup', up);\n });\n el.addEventListener('keydown', (e) => {\n const { step, min, max } = P(), d = step > 0 ? step : (max - min) / 100;\n if(e.key === 'ArrowRight' || e.key === 'ArrowUp'){ setV(k, (st[k] ?? min) + d); commit(k); e.preventDefault(); }\n if(e.key === 'ArrowLeft' || e.key === 'ArrowDown'){ setV(k, (st[k] ?? min) - d); commit(k); e.preventDefault(); }\n });\n }\n const sub = () => {\n dbSubscribe(instance, 'signal', instance.signal, (v) => { st.lo = dbNum(v, null); draw(); });\n if(instance.signalHigh) dbSubscribe(instance, 'signalHigh', instance.signalHigh, (v) => { st.hi = dbNum(v, null); draw(); });\n else dbUnsub(instance, 'signalHigh');\n draw();\n };\n for(const p of ['label', 'min', 'max', 'step', 'unit', 'decimals', 'show-label']) instance._assignEvent(p + '-changed', draw);\n instance._assignEvent('signal-changed', sub); instance._assignEvent('signal-high-changed', sub);\n sub();\n}\nexport function disconnectedCallback(instance){ dbUnsubAll(instance); }\n"
47
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "properties": {
3
+ "active": {
4
+ "type": "boolean",
5
+ "default": "true"
6
+ },
7
+ "textOn": {
8
+ "type": "string",
9
+ "default": "HEATING ON"
10
+ },
11
+ "textOff": {
12
+ "type": "string",
13
+ "default": "HEATING OFF"
14
+ },
15
+ "toneOn": {
16
+ "type": "enum",
17
+ "values": [
18
+ "ok",
19
+ "warn",
20
+ "error",
21
+ "info",
22
+ "accent",
23
+ "glow",
24
+ "off"
25
+ ],
26
+ "default": "ok"
27
+ },
28
+ "toneOff": {
29
+ "type": "enum",
30
+ "values": [
31
+ "ok",
32
+ "warn",
33
+ "error",
34
+ "info",
35
+ "accent",
36
+ "glow",
37
+ "off"
38
+ ],
39
+ "default": "off"
40
+ },
41
+ "variant": {
42
+ "type": "enum",
43
+ "values": [
44
+ "text",
45
+ "chip",
46
+ "icon"
47
+ ],
48
+ "default": "text"
49
+ },
50
+ "icon": {
51
+ "type": "enum",
52
+ "values": [
53
+ "none",
54
+ "relay",
55
+ "pump",
56
+ "bell",
57
+ "flame",
58
+ "check"
59
+ ],
60
+ "default": "none"
61
+ },
62
+ "signal": {
63
+ "type": "signal",
64
+ "default": ""
65
+ }
66
+ },
67
+ "settings": {
68
+ "width": "190px",
69
+ "height": "40px"
70
+ },
71
+ "html": "<button id=\"c\" class=\"sc\" type=\"button\"><span id=\"ic\" class=\"ic\"></span><span id=\"tx\" class=\"tx\"></span></button>",
72
+ "style": ":host{display:block;box-sizing:border-box;font-family:'IBM Plex Sans','Segoe UI',system-ui,sans-serif;color:var(--ui-text,#e6ebf2);--db-glow:var(--ui-glow,#ffd36a)}\n*{box-sizing:border-box}\n.mono{font-family:'IBM Plex Mono',ui-monospace,Consolas,monospace;font-variant-numeric:tabular-nums}\n@media (prefers-reduced-motion: reduce){*{animation:none!important}}\n.t-ok{--tone:var(--ui-ok,#3ccf7a)} .t-warn{--tone:var(--ui-warn,#f2a531)} .t-error{--tone:var(--ui-error,#ff5a6a)}\n.t-info{--tone:var(--ui-info,#57b6ff)} .t-accent{--tone:var(--accent,#57b6ff)} .t-glow{--tone:var(--db-glow)} .t-off{--tone:var(--ui-dim,#9aa5b5)}\n.sc{height:100%;width:100%;display:flex;align-items:center;gap:8px;border:0;background:none;color:var(--tone);font:600 15px 'IBM Plex Sans',sans-serif;letter-spacing:.06em;padding:0;cursor:default;text-align:left}\n.sc.click{cursor:pointer}\n.sc.chip{justify-content:center;text-align:center;padding:6px 14px;border-radius:9px;font-size:13px;line-height:1.25;white-space:pre-line;background:linear-gradient(180deg,#e9ecf1,#c4cad3);color:#38404c}\n.sc.chip.on{color:#3a2a00;background:linear-gradient(180deg,color-mix(in srgb,var(--tone) 45%,#fff),var(--tone));box-shadow:0 0 14px color-mix(in srgb,var(--tone) 60%,transparent)}\n.sc.icon{justify-content:center;border-radius:8px;background:var(--ui-surface-2,#3a414d);border:1px solid var(--ui-border,#4a5262);color:var(--ui-dim,#9aa5b5)}\n.sc.icon.on{color:var(--tone);box-shadow:0 0 0 1px var(--tone)}\n.sc.icon .tx{display:none}\n.ic:empty{display:none}.ic svg{width:26px;height:22px;display:block}\n.sc:focus-visible{outline:2px solid var(--accent,#57b6ff);outline-offset:2px}\n",
73
+ "script": "function dbSubscribe(instance, key, id, onVal){\n dbUnsub(instance, key);\n if(!id || !window.IOB) return;\n instance.__dbSubs = instance.__dbSubs || {};\n const cb = function(_i, s){ onVal(s ? s.val : null); };\n instance.__dbSubs[key] = [id, cb];\n try{ window.IOB.subscribeState(id, cb); }catch(e){}\n try{ window.IOB.getState(id).then(function(s){ onVal(s ? s.val : null); }).catch(function(){}); }catch(e){}\n // read quality knows this state (badge on the control when the value cannot be trusted)\n try{ instance.__dbRq = instance.__dbRq || {}; instance.__dbRq[key] = window.IOB.watchReadQuality && window.IOB.watchReadQuality(instance, id, { prop: key, twoWay: true }); }catch(e){}\n}\nfunction dbUnsub(instance, key){\n const p = instance.__dbSubs && instance.__dbSubs[key];\n if(p){ try{ window.IOB.unsubscribeState(p[0], p[1]); }catch(e){} delete instance.__dbSubs[key]; }\n const r = instance.__dbRq && instance.__dbRq[key];\n if(typeof r === 'function'){ try{ r(); }catch(e){} }\n if(instance.__dbRq) delete instance.__dbRq[key];\n}\nfunction dbUnsubAll(instance){ for(const k of Object.keys(instance.__dbSubs || {})) dbUnsub(instance, k); }\n// a write through IOB.setState: Confirm & Protect may ask first; cancelled = nothing written\nfunction dbWrite(instance, id, val, onCancel){\n if(!id || !window.IOB) return Promise.resolve(false);\n return Promise.resolve(window.IOB.setState(id, val, false, { feedback: instance })).then(function(){ return true; }, function(e){\n if(e && e.cancelled){ if(onCancel) onCancel(); }\n else console.warn('[dashboard] write failed', id, e);\n return false;\n });\n}\nfunction dbBool(v){ return v === true || v === 1 || v === 'true' || v === '1' || v === 'on'; }\nfunction dbNum(v, d){ const n = Number(v); return isFinite(n) ? n : d; }\nconst ICONS = {\"none\": \"\", \"relay\": \"<svg viewBox=\\\"0 0 26 22\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\"><rect x=\\\"1\\\" y=\\\"1\\\" width=\\\"24\\\" height=\\\"20\\\" rx=\\\"3\\\"/><path d=\\\"M5 14h4l3-7 3 9 2-4h4\\\"/></svg>\", \"pump\": \"<svg viewBox=\\\"0 0 26 22\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2\\\"><rect x=\\\"1\\\" y=\\\"3\\\" width=\\\"24\\\" height=\\\"16\\\" rx=\\\"3\\\"/><path d=\\\"M8 7l9 4-9 4z\\\" fill=\\\"currentColor\\\"/></svg>\", \"bell\": \"<svg viewBox=\\\"0 0 24 24\\\" fill=\\\"currentColor\\\"><path d=\\\"M12 22a2.5 2.5 0 0 0 2.4-2h-4.8A2.5 2.5 0 0 0 12 22zm7-6V11a7 7 0 0 0-5.5-6.8V3.5a1.5 1.5 0 0 0-3 0v.7A7 7 0 0 0 5 11v5l-2 2v1h18v-1z\\\"/></svg>\", \"flame\": \"<svg viewBox=\\\"0 0 24 24\\\" fill=\\\"currentColor\\\"><path d=\\\"M12 2c1 4 6 6 6 12a6 6 0 1 1-12 0c0-3 2-5 3-6 0 3 2 4 3 4-1-4 1-7 0-10z\\\"/></svg>\", \"check\": \"<svg viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2.4\\\"><path d=\\\"M4 12l5 5L20 6\\\"/></svg>\"};\nexport function init(instance){\n const $ = (id) => instance._getDomElement(id);\n let stateVal = null;\n const isOn = () => instance.signal ? dbBool(stateVal) : dbBool(instance.active);\n const upd = () => {\n const on = isOn(), c = $('c'), v = instance.variant || 'text';\n c.className = 'sc ' + v + (on ? ' on' : '') + (instance.signal ? ' click' : '') + ' t-' + (on ? (instance.toneOn || 'ok') : (instance.toneOff || 'off'));\n $('tx').textContent = on ? (instance.textOn || '') : (instance.textOff || '');\n $('ic').innerHTML = ICONS[instance.icon] || '';\n c.setAttribute('aria-pressed', String(on));\n c.title = $('tx').textContent;\n };\n // with a signal the chip is a switch: a click writes the opposite\n $('c').addEventListener('click', () => { if(instance.signal) dbWrite(instance, instance.signal, !dbBool(stateVal)); });\n const sub = () => { if(instance.signal) dbSubscribe(instance, 'signal', instance.signal, (v) => { stateVal = v; upd(); }); else dbUnsub(instance, 'signal'); upd(); };\n for(const p of ['active', 'text-on', 'text-off', 'tone-on', 'tone-off', 'variant', 'icon']) instance._assignEvent(p + '-changed', upd);\n instance._assignEvent('signal-changed', sub);\n sub();\n}\nexport function disconnectedCallback(instance){ dbUnsubAll(instance); }\n"
74
+ }