iobroker.mywebui 1.42.45 → 1.42.46

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.42.45",
3
+ "version": "1.42.46",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413 with 3D Editor",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -68,6 +68,7 @@
68
68
  "dependencies": {
69
69
  "@iobroker/adapter-core": "^3.3.2",
70
70
  "@types/node": "^24.7.2",
71
+ "esbuild": "^0.28.1",
71
72
  "three": "^0.184.0"
72
73
  },
73
74
  "devDependencies": {
@@ -112,4 +113,4 @@
112
113
  "typescript-json-schema": "^0.65.1",
113
114
  "wunderbaum": "0.13.0"
114
115
  }
115
- }
116
+ }
@@ -1,34 +1,19 @@
1
1
  import { BaseCustomWebComponentConstructorAppend, css, html } from "@gokturk413/base-custom-webcomponent";
2
2
 
3
- const EDITOR_BASE = new URL('../../../3d-editor/', import.meta.url).href;
4
-
5
3
  export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponentConstructorAppend {
6
4
 
7
5
  static template = html`
8
6
  <div id="root">
9
- <!-- Sub-tab bar -->
10
- <div id="subBar">
11
- <span class="stab active" data-sub="scene">Scene</span>
12
- <span class="stab" data-sub="object">Object</span>
13
- <span class="stab" data-sub="geom">Geom</span>
14
- <span class="stab" data-sub="mater">Mater</span>
15
- </div>
16
-
17
- <!-- Scene tab -->
18
- <div id="subScene" class="spanel active"></div>
19
7
 
20
- <!-- Object tab -->
21
- <div id="subObject" class="spanel" style="display:none;">
22
- <div id="objThreeWrap"></div>
23
- <div class="sechdr">BINDINGS (IOBROKER STATES)</div>
24
- <div id="bindRows"></div>
8
+ <!-- Selected object header -->
9
+ <div id="objHeader">
10
+ <span id="objType" class="htype"></span>
11
+ <span id="objName" class="hname">No object selected</span>
25
12
  </div>
26
13
 
27
- <!-- Geom tab -->
28
- <div id="subGeom" class="spanel" style="display:none;"></div>
29
-
30
- <!-- Mater tab -->
31
- <div id="subMater" class="spanel" style="display:none;"></div>
14
+ <!-- Section: 3D Bindings -->
15
+ <div class="sechdr">3D BINDINGS (IOBROKER STATES)</div>
16
+ <div id="bindRows"></div>
32
17
 
33
18
  <!-- Context menu -->
34
19
  <div id="ctxMenu">
@@ -40,12 +25,12 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
40
25
  <!-- Binding dialog -->
41
26
  <div id="bdOverlay">
42
27
  <div id="bdBox">
43
- <div id="bdTitle">Edit Binding of '<span id="bdPropLabel"></span>' - 3DScreen</div>
28
+ <div id="bdTitle">Edit Binding of '<span id="bdPropLabel"></span>'</div>
44
29
 
45
30
  <div class="bdRow">
46
31
  <div class="bdLbl">objects</div>
47
- <div style="display:flex;gap:3px;align-items:center;">
48
- <input id="bdObj" type="text" class="bdInput" style="flex:1;" placeholder="state id" autocomplete="off"/>
32
+ <div style="display:flex;gap:3px;">
33
+ <input id="bdObj" type="text" class="bdInput" style="flex:1;" placeholder="mywebui.0.state.value" autocomplete="off"/>
49
34
  <button class="bdBtn sm" id="bdObjX">X</button>
50
35
  <button class="bdBtn sm" id="bdObjBrowse">...</button>
51
36
  <button class="bdBtn sm accent" id="bdObjIOB">IOB</button>
@@ -53,20 +38,20 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
53
38
  </div>
54
39
 
55
40
  <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
56
- <span class="bdLbl" style="margin-bottom:0;">type :</span>
41
+ <span class="bdLbl" style="margin-bottom:0;min-width:40px;">type :</span>
57
42
  <select id="bdType" class="bdSel">
58
43
  <option value="signal">signal</option>
59
- <option value="ignore" selected>ignore</option>
44
+ <option value="ignore">ignore</option>
60
45
  <option value="css">css</option>
61
46
  <option value="attribute">attribute</option>
62
47
  </select>
63
48
  </div>
64
49
 
65
- <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
50
+ <div class="bdRow bdChkRow">
66
51
  <input id="bdTwoWay" type="checkbox"/>
67
52
  <span class="bdChkLbl">two way binding</span>
68
53
  </div>
69
- <div class="bdRow" style="display:flex;align-items:center;gap:8px;">
54
+ <div class="bdRow bdChkRow">
70
55
  <input id="bdInvert" type="checkbox"/>
71
56
  <span class="bdChkLbl">invert logic</span>
72
57
  </div>
@@ -80,24 +65,24 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
80
65
  </div>
81
66
 
82
67
  <div class="bdRow">
83
- <span class="bdLbl">write back signal :</span>
84
- <input id="bdWriteBack" type="text" class="bdInput" placeholder=""/>
68
+ <div class="bdLbl">write back signal :</div>
69
+ <input id="bdWriteBack" type="text" class="bdInput"/>
85
70
  </div>
86
71
 
87
72
  <div class="bdRow">
88
- <span class="bdLbl">formula write back (two way)</span>
73
+ <div class="bdLbl">formula write back (two way)</div>
89
74
  <textarea id="bdFormulaWB" class="bdTa" rows="2" placeholder="val"></textarea>
90
75
  </div>
91
76
 
92
77
  <div class="bdRow">
93
- <span class="bdLbl">converter:</span>
94
- <table id="bdConvTable" class="bdTable">
78
+ <div class="bdLbl">converter:</div>
79
+ <table class="bdTable">
95
80
  <thead><tr><th>condition</th><th>value</th></tr></thead>
96
81
  <tbody id="bdConvBody"></tbody>
97
82
  </table>
98
83
  <div style="display:flex;gap:4px;margin-top:4px;justify-content:flex-end;">
99
- <button class="bdBtn" id="bdConvAdd">add</button>
100
- <button class="bdBtn" id="bdConvRemove">remove</button>
84
+ <button class="bdBtn sm" id="bdConvAdd">add</button>
85
+ <button class="bdBtn sm" id="bdConvRemove">remove</button>
101
86
  </div>
102
87
  </div>
103
88
 
@@ -112,159 +97,225 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
112
97
 
113
98
  static style = css`
114
99
  :host { display:block;width:100%;height:100%;overflow:hidden; }
115
- #root { width:100%;height:100%;display:flex;flex-direction:column;overflow:hidden;background:#1a1a1a;color:#ccc;font-family:Helvetica,Arial,sans-serif;font-size:12px;position:relative; }
116
100
 
117
- /* Sub-tab bar */
118
- #subBar { display:flex;background:#2d2d30;border-bottom:1px solid #3c3c3c;flex-shrink:0; }
119
- .stab { padding:5px 10px;cursor:pointer;font-size:11px;color:#888;border-right:1px solid #3c3c3c;white-space:nowrap;user-select:none; }
120
- .stab:hover { color:#ddd;background:#3e3e42; }
121
- .stab.active { color:#fff;background:#1e1e1e;border-top:2px solid #4ec9b0; }
101
+ #root {
102
+ width:100%;height:100%;
103
+ display:flex;flex-direction:column;
104
+ overflow:hidden;
105
+ background:#1e1e1e;
106
+ color:#d4d4d4;
107
+ font-family:Segoe UI,Helvetica,Arial,sans-serif;
108
+ font-size:12px;
109
+ position:relative;
110
+ }
122
111
 
123
- /* Sub-tab panels */
124
- .spanel { flex:1;overflow:auto;min-height:0; }
125
- #subScene,#subGeom,#subMater { overflow:auto; }
126
- #subObject { display:flex;flex-direction:column; }
127
- #objThreeWrap { overflow:auto; }
112
+ /* Selected object header */
113
+ #objHeader {
114
+ display:flex;align-items:center;gap:6px;
115
+ padding:5px 8px;
116
+ background:#252526;
117
+ border-bottom:1px solid #3c3c3c;
118
+ flex-shrink:0;
119
+ min-height:26px;
120
+ }
121
+ .htype {
122
+ font-size:10px;color:#569cd6;
123
+ background:#1e1e1e;border:1px solid #3c3c3c;
124
+ padding:1px 5px;border-radius:2px;
125
+ white-space:nowrap;
126
+ }
127
+ .hname { font-size:11px;color:#9cdcfe;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
128
128
 
129
129
  /* Section header */
130
- .sechdr { background:#252526;color:#9cdcfe;padding:4px 8px;font-size:10px;font-weight:bold;text-transform:uppercase;border-top:1px solid #3c3c3c;border-bottom:1px solid #3c3c3c;letter-spacing:.5px;flex-shrink:0; }
131
-
132
- /* Binding rows — match 2D property grid style */
133
- #bindRows { flex:1;overflow:auto; }
134
- .brow { display:flex;align-items:center;padding:2px 4px;gap:0;border-bottom:1px solid #2a2a2a;min-height:20px; }
135
- .brow:hover { background:#252526; }
136
- /* □ square button */
137
- .bsq { width:14px;height:14px;min-width:14px;border:1px solid #555;background:#2d2d30;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:8px;color:#888;flex-shrink:0;margin-right:4px;padding:0; }
130
+ .sechdr {
131
+ background:#252526;
132
+ color:#9cdcfe;
133
+ padding:4px 8px;
134
+ font-size:10px;font-weight:700;
135
+ text-transform:uppercase;
136
+ letter-spacing:.6px;
137
+ border-bottom:1px solid #3c3c3c;
138
+ flex-shrink:0;
139
+ }
140
+
141
+ /* Binding rows */
142
+ #bindRows { flex:1;overflow-y:auto;overflow-x:hidden; }
143
+
144
+ .brow {
145
+ display:flex;align-items:center;
146
+ padding:0 6px;
147
+ height:22px;
148
+ border-bottom:1px solid #2a2a2a;
149
+ gap:0;
150
+ }
151
+ .brow:hover { background:#2a2d2e; }
152
+
153
+ /* □ square binding button */
154
+ .bsq {
155
+ width:13px;height:13px;min-width:13px;
156
+ border:1px solid #4d4d4d;
157
+ background:transparent;
158
+ cursor:pointer;
159
+ display:flex;align-items:center;justify-content:center;
160
+ font-size:7px;color:transparent;
161
+ flex-shrink:0;
162
+ margin-right:5px;
163
+ padding:0;
164
+ outline:none;
165
+ }
138
166
  .bsq:hover { border-color:#4ec9b0; }
139
- .bsq.bound { background:#0e639c;border-color:#1177bb;color:#fff; }
140
- /* property name */
141
- .blbl { width:90px;color:#9cdcfe;font-size:11px;flex-shrink:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
142
- /* signal display */
143
- .bval { flex:1;color:#4ec9b0;font-size:10px;font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 2px; }
167
+ .bsq.bound {
168
+ background:#0e639c;
169
+ border-color:#1177bb;
170
+ color:#fff;
171
+ font-size:8px;
172
+ }
173
+
174
+ /* Property label */
175
+ .blbl {
176
+ width:88px;min-width:88px;
177
+ color:#d4d4d4;
178
+ font-size:11px;
179
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
180
+ flex-shrink:0;
181
+ }
182
+
183
+ /* Signal value display */
184
+ .bval {
185
+ flex:1;
186
+ font-size:10px;
187
+ font-family:Consolas,monospace;
188
+ overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
189
+ padding:0 3px;
190
+ color:#4ec9b0;
191
+ }
144
192
  .bval.none { color:#555; }
145
193
 
146
194
  /* Context menu */
147
- #ctxMenu { display:none;position:fixed;z-index:9999;background:#2d2d30;border:1px solid #555;min-width:120px;box-shadow:2px 2px 8px rgba(0,0,0,0.6); }
195
+ #ctxMenu {
196
+ display:none;position:fixed;z-index:9999;
197
+ background:#252526;border:1px solid #454545;
198
+ min-width:130px;
199
+ box-shadow:2px 4px 12px rgba(0,0,0,.7);
200
+ }
148
201
  #ctxMenu.open { display:block; }
149
- .cmItem { padding:5px 12px;cursor:pointer;font-size:11px;color:#ccc; }
150
- .cmItem:hover { background:#3e3e42;color:#fff; }
151
-
152
- /* Binding dialog overlay */
153
- #bdOverlay { display:none;position:absolute;inset:0;background:rgba(0,0,0,0.75);z-index:500;align-items:center;justify-content:center; }
202
+ .cmItem { padding:5px 14px;cursor:pointer;font-size:11px;color:#d4d4d4; }
203
+ .cmItem:hover { background:#094771;color:#fff; }
204
+
205
+ /* Binding dialog */
206
+ #bdOverlay {
207
+ display:none;position:absolute;inset:0;
208
+ background:rgba(0,0,0,.72);
209
+ z-index:500;
210
+ align-items:center;justify-content:center;
211
+ }
154
212
  #bdOverlay.open { display:flex; }
155
- #bdBox { background:#2d2d30;border:1px solid #555;padding:0;width:340px;max-height:90%;overflow-y:auto;box-shadow:4px 4px 16px rgba(0,0,0,0.7); }
156
- #bdTitle { background:#3e3e42;padding:7px 10px;font-size:12px;font-weight:bold;color:#9cdcfe;border-bottom:1px solid #555;margin-bottom:0; }
157
- .bdRow { padding:4px 10px 2px; }
158
- .bdLbl { color:#888;font-size:10px;margin-bottom:2px;display:block; }
159
- .bdInput { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box; }
160
- .bdInput:focus { border-color:#4ec9b0;outline:none; }
161
- .bdSel { background:#3c3c3c;border:1px solid #555;color:#ccc;padding:2px 4px;font-size:11px; }
162
- .bdTa { width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:3px 5px;font-size:11px;box-sizing:border-box;resize:vertical;font-family:monospace; }
163
- .bdTa:focus { border-color:#4ec9b0;outline:none; }
164
- .bdChkLbl { color:#ccc;font-size:11px; }
165
- .bdTable { width:100%;border-collapse:collapse;font-size:10px;color:#ccc; }
166
- .bdTable th { background:#252526;padding:2px 6px;text-align:left;color:#888;border:1px solid #3c3c3c; }
167
- .bdTable td { padding:2px 4px;border:1px solid #3c3c3c; }
168
- .bdTable td input { width:100%;background:#3c3c3c;border:none;color:#ccc;padding:1px 3px;font-size:10px; }
169
- .bdBtn { padding:3px 10px;background:#3c3c3c;border:1px solid #555;color:#ccc;cursor:pointer;font-size:11px; }
213
+
214
+ #bdBox {
215
+ background:#252526;
216
+ border:1px solid #454545;
217
+ width:320px;max-height:88%;
218
+ overflow-y:auto;
219
+ box-shadow:4px 6px 20px rgba(0,0,0,.8);
220
+ }
221
+ #bdTitle {
222
+ background:#37373d;
223
+ padding:7px 10px;
224
+ font-size:11px;font-weight:600;
225
+ color:#ccc;
226
+ border-bottom:1px solid #454545;
227
+ }
228
+ .bdRow { padding:5px 10px 2px; }
229
+ .bdChkRow { display:flex;align-items:center;gap:8px;padding:3px 10px; }
230
+ .bdLbl { color:#888;font-size:10px;margin-bottom:3px;display:block; }
231
+ .bdChkLbl { font-size:11px;color:#d4d4d4; }
232
+ .bdInput {
233
+ width:100%;
234
+ background:#3c3c3c;border:1px solid #555;
235
+ color:#d4d4d4;padding:3px 5px;
236
+ font-size:11px;box-sizing:border-box;
237
+ outline:none;
238
+ }
239
+ .bdInput:focus { border-color:#4ec9b0; }
240
+ .bdSel {
241
+ background:#3c3c3c;border:1px solid #555;
242
+ color:#d4d4d4;padding:2px 4px;font-size:11px;
243
+ outline:none;
244
+ }
245
+ .bdTa {
246
+ width:100%;background:#3c3c3c;border:1px solid #555;
247
+ color:#d4d4d4;padding:3px 5px;font-size:11px;
248
+ font-family:Consolas,monospace;
249
+ box-sizing:border-box;resize:vertical;
250
+ outline:none;
251
+ }
252
+ .bdTa:focus { border-color:#4ec9b0; }
253
+ .bdTable { width:100%;border-collapse:collapse;font-size:10px;color:#d4d4d4; }
254
+ .bdTable th { background:#1e1e1e;padding:2px 6px;text-align:left;color:#888;border:1px solid #3c3c3c; }
255
+ .bdTable td { padding:1px 3px;border:1px solid #3c3c3c; }
256
+ .bdTable td input { width:100%;background:#3c3c3c;border:none;color:#d4d4d4;padding:2px 3px;font-size:10px;outline:none; }
257
+ .bdBtn {
258
+ padding:3px 10px;
259
+ background:#3c3c3c;border:1px solid #555;
260
+ color:#d4d4d4;cursor:pointer;font-size:11px;
261
+ }
170
262
  .bdBtn:hover { border-color:#999; }
171
263
  .bdBtn.sm { padding:2px 6px;font-size:10px; }
172
264
  .bdBtn.accent { background:#0e639c;border-color:#1177bb;color:#fff; }
173
265
  .bdBtn.accent-ok { background:#0e639c;border-color:#1177bb;color:#fff; }
174
- #bdFoot { display:flex;justify-content:flex-end;gap:6px;padding:8px 10px;border-top:1px solid #3c3c3c;margin-top:4px; }
266
+ #bdFoot {
267
+ display:flex;justify-content:flex-end;gap:6px;
268
+ padding:8px 10px;border-top:1px solid #3c3c3c;
269
+ }
175
270
  `;
176
271
 
177
- _editor = null;
178
- _editorHost = null;
179
- _bindings = {};
272
+ _editor = null;
273
+ _editorHost = null;
274
+ _bindings = {};
180
275
  _selectedObj = null;
181
- _ctxTarget = null; // { uuid, key }
276
+ _ctxTarget = null;
182
277
 
183
278
  // ── Public API ─────────────────────────────────────────────────────────────
184
279
 
185
280
  async connect(editor, editorHost) {
186
- this._editor = editor;
281
+ this._editor = editor;
187
282
  this._editorHost = editorHost;
188
- this._bindings = editorHost?.sceneData?.bindings ?? {};
283
+ this._bindings = editorHost?.sceneData?.bindings ?? {};
189
284
 
190
- await this._injectThreeCSS();
191
- await this._mountThreePanels();
192
- this._setupSubTabs();
193
285
  this._buildBindingRows();
194
286
  this._setupContextMenu();
195
287
  this._setupBindingDialog();
196
288
 
197
289
  editor.signals.objectSelected.add(obj => {
198
290
  this._selectedObj = obj;
291
+ this._refreshHeader();
199
292
  this._refreshBindRows();
200
293
  });
201
294
  editor.signals.objectChanged.add(obj => {
202
295
  if (obj && obj === this._selectedObj) this._refreshBindRows();
203
296
  });
204
297
 
298
+ this._refreshHeader();
205
299
  this._refreshBindRows();
206
300
  }
207
301
 
208
302
  getBindings() { return this._bindings; }
209
-
210
- setBindings(b) {
211
- this._bindings = b || {};
212
- this._refreshBindRows();
213
- }
214
-
215
- // ── Three.js CSS ────────────────────────────────────────────────────────────
216
-
217
- async _injectThreeCSS() {
218
- const sr = this.shadowRoot;
219
- if (!sr || sr.querySelector('#tcss')) return;
220
- const l = document.createElement('link');
221
- l.id = 'tcss'; l.rel = 'stylesheet';
222
- l.href = EDITOR_BASE + 'css/main.css';
223
- sr.appendChild(l);
224
- }
225
-
226
- // ── Mount Three.js sidebar panels ──────────────────────────────────────────
227
-
228
- async _mountThreePanels() {
229
- const base = EDITOR_BASE + 'js/';
230
- const ed = this._editor;
231
- const [
232
- { SidebarScene },
233
- { SidebarObject },
234
- { SidebarGeometry },
235
- { SidebarMaterial },
236
- ] = await Promise.all([
237
- import(/* @vite-ignore */ base + 'Sidebar.Scene.js'),
238
- import(/* @vite-ignore */ base + 'Sidebar.Object.js'),
239
- import(/* @vite-ignore */ base + 'Sidebar.Geometry.js'),
240
- import(/* @vite-ignore */ base + 'Sidebar.Material.js'),
241
- ]);
242
-
243
- this._getDomElement('subScene').appendChild(new SidebarScene(ed).dom);
244
- this._getDomElement('objThreeWrap').appendChild(new SidebarObject(ed).dom);
245
- this._getDomElement('subGeom').appendChild(new SidebarGeometry(ed).dom);
246
- this._getDomElement('subMater').appendChild(new SidebarMaterial(ed).dom);
247
- }
248
-
249
- // ── Sub-tabs ────────────────────────────────────────────────────────────────
250
-
251
- _setupSubTabs() {
252
- const bar = this._getDomElement('subBar');
253
- const panels = {
254
- scene: this._getDomElement('subScene'),
255
- object: this._getDomElement('subObject'),
256
- geom: this._getDomElement('subGeom'),
257
- mater: this._getDomElement('subMater'),
258
- };
259
- bar?.querySelectorAll('.stab').forEach(btn => {
260
- btn.addEventListener('click', () => {
261
- bar.querySelectorAll('.stab').forEach(b => b.classList.remove('active'));
262
- btn.classList.add('active');
263
- Object.values(panels).forEach(p => { if (p) p.style.display = 'none'; });
264
- const p = panels[btn.dataset.sub];
265
- if (p) p.style.display = btn.dataset.sub === 'object' ? 'flex' : 'block';
266
- });
267
- });
303
+ setBindings(b) { this._bindings = b || {}; this._refreshBindRows(); }
304
+
305
+ // ── Header ─────────────────────────────────────────────────────────────────
306
+
307
+ _refreshHeader() {
308
+ const obj = this._selectedObj;
309
+ const typeEl = this._getDomElement('objType');
310
+ const nameEl = this._getDomElement('objName');
311
+ if (!typeEl || !nameEl) return;
312
+ if (!obj) {
313
+ typeEl.textContent = '';
314
+ nameEl.textContent = 'No object selected';
315
+ } else {
316
+ typeEl.textContent = obj.type || '';
317
+ nameEl.textContent = obj.name || obj.uuid?.slice(0, 8) || '—';
318
+ }
268
319
  }
269
320
 
270
321
  // ── Bindable properties ─────────────────────────────────────────────────────
@@ -288,60 +339,44 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
288
339
  const cont = this._getDomElement('bindRows');
289
340
  if (!cont) return;
290
341
  cont.innerHTML = '';
291
- this._BINDABLE.forEach(({ key, label }) => {
342
+ for (const { key, label } of this._BINDABLE) {
292
343
  const row = document.createElement('div');
293
344
  row.className = 'brow';
294
345
  row.dataset.key = key;
295
346
 
296
- // □ square button (left of label)
297
347
  const sq = document.createElement('button');
298
348
  sq.className = 'bsq';
299
- sq.title = 'right-click to bind / clear';
300
- sq.textContent = '■';
301
- sq.addEventListener('contextmenu', e => {
302
- e.preventDefault();
303
- this._showCtxMenu(e, key);
304
- });
305
- sq.addEventListener('click', e => {
306
- e.preventDefault();
307
- this._showCtxMenu(e, key);
308
- });
349
+ sq.title = key;
350
+ const onClick = e => { e.preventDefault(); this._showCtxMenu(e, key); };
351
+ sq.addEventListener('click', onClick);
352
+ sq.addEventListener('contextmenu', onClick);
309
353
 
310
- // property name
311
354
  const lbl = document.createElement('span');
312
355
  lbl.className = 'blbl';
313
356
  lbl.textContent = label;
314
357
 
315
- // current signal display
316
358
  const val = document.createElement('span');
317
359
  val.className = 'bval none';
318
- val.textContent = '';
319
360
 
320
361
  row.appendChild(sq);
321
362
  row.appendChild(lbl);
322
363
  row.appendChild(val);
323
364
  cont.appendChild(row);
324
- });
365
+ }
325
366
  }
326
367
 
327
368
  _refreshBindRows() {
328
369
  const cont = this._getDomElement('bindRows');
329
370
  if (!cont) return;
330
371
  const uuid = this._selectedObj?.uuid;
331
- cont.querySelectorAll('.brow').forEach(row => {
372
+ for (const row of cont.querySelectorAll('.brow')) {
332
373
  const key = row.dataset.key;
333
374
  const sq = row.querySelector('.bsq');
334
375
  const val = row.querySelector('.bval');
335
376
  const bind = uuid ? this._bindings[uuid]?.[key] : null;
336
-
337
- if (val) {
338
- val.textContent = bind?.signal || '';
339
- val.className = 'bval' + (bind?.signal ? '' : ' none');
340
- }
341
- if (sq) {
342
- sq.className = 'bsq' + (bind?.signal ? ' bound' : '');
343
- }
344
- });
377
+ if (sq) sq.className = 'bsq' + (bind?.signal ? ' bound' : '');
378
+ if (val) { val.textContent = bind?.signal || ''; val.className = 'bval' + (bind?.signal ? '' : ' none'); }
379
+ }
345
380
  }
346
381
 
347
382
  // ── Context menu ────────────────────────────────────────────────────────────
@@ -349,33 +384,25 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
349
384
  _setupContextMenu() {
350
385
  const menu = this._getDomElement('ctxMenu');
351
386
  if (!menu) return;
352
-
353
387
  menu.addEventListener('click', e => {
354
388
  const item = e.target.closest('.cmItem');
355
389
  if (!item) return;
356
- const action = item.dataset.action;
357
390
  menu.classList.remove('open');
358
-
391
+ const action = item.dataset.action;
359
392
  if (action === 'clear') this._clearBinding(this._ctxTarget);
360
393
  if (action === 'editbind') this._openBindDialog(this._ctxTarget);
361
394
  if (action === 'edittext') this._openTextEditor(this._ctxTarget);
362
395
  });
363
-
364
- // close on outside click
365
396
  document.addEventListener('click', e => {
366
397
  if (!menu.contains(e.target)) menu.classList.remove('open');
367
398
  });
368
399
  }
369
400
 
370
401
  _showCtxMenu(e, key) {
371
- if (!this._selectedObj) {
372
- alert('Select a 3D object first.');
373
- return;
374
- }
402
+ if (!this._selectedObj) { alert('Select a 3D object first.'); return; }
375
403
  this._ctxTarget = { uuid: this._selectedObj.uuid, key };
376
404
  const menu = this._getDomElement('ctxMenu');
377
405
  if (!menu) return;
378
- // position near click
379
406
  menu.style.left = e.clientX + 'px';
380
407
  menu.style.top = e.clientY + 'px';
381
408
  menu.classList.add('open');
@@ -386,21 +413,18 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
386
413
  delete this._bindings[uuid]?.[key];
387
414
  if (this._bindings[uuid] && !Object.keys(this._bindings[uuid]).length)
388
415
  delete this._bindings[uuid];
389
- this._syncHost();
390
- this._refreshBindRows();
416
+ this._syncHost(); this._refreshBindRows();
391
417
  }
392
418
 
393
419
  _openTextEditor({ uuid, key } = {}) {
394
420
  if (!uuid || !key) return;
395
421
  const ex = this._bindings[uuid]?.[key];
396
- const val = ex?.signal || '';
397
- const nv = window.prompt(`State ID for "${key}":`, val);
422
+ const nv = window.prompt(`State ID for "${key}":`, ex?.signal || '');
398
423
  if (nv === null) return;
399
424
  if (!nv.trim()) { this._clearBinding({ uuid, key }); return; }
400
425
  if (!this._bindings[uuid]) this._bindings[uuid] = {};
401
- this._bindings[uuid][key] = { signal: nv.trim(), formula: ex?.formula || 'val', twoWay: ex?.twoWay || false };
402
- this._syncHost();
403
- this._refreshBindRows();
426
+ this._bindings[uuid][key] = { ...(ex || {}), signal: nv.trim(), formula: ex?.formula || 'val' };
427
+ this._syncHost(); this._refreshBindRows();
404
428
  }
405
429
 
406
430
  // ── Binding dialog ──────────────────────────────────────────────────────────
@@ -408,36 +432,22 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
408
432
  _setupBindingDialog() {
409
433
  const ov = this._getDomElement('bdOverlay');
410
434
 
411
- this._getDomElement('bdCancel')?.addEventListener('click', () => {
412
- ov?.classList.remove('open');
413
- });
435
+ this._getDomElement('bdCancel')?.addEventListener('click', () => ov?.classList.remove('open'));
414
436
 
415
437
  this._getDomElement('bdObjX')?.addEventListener('click', () => {
416
- const inp = this._getDomElement('bdObj');
417
- if (inp) inp.value = '';
418
- });
419
-
420
- this._getDomElement('bdObjBrowse')?.addEventListener('click', async () => {
421
- try {
422
- const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
423
- const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
424
- const id = await openSelectIdDialog(connection, null, false);
425
- if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
426
- } catch (_) {}
438
+ const inp = this._getDomElement('bdObj'); if (inp) inp.value = '';
427
439
  });
428
440
 
429
- this._getDomElement('bdObjIOB')?.addEventListener('click', async () => {
441
+ const browse = async () => {
430
442
  try {
431
443
  const { openSelectIdDialog } = await import('@iobroker/webcomponent-selectid-dialog/dist/selectIdHelper.js');
432
- const connection = (await import('../common/IobrokerHandler.js')).iobrokerHandler.connection;
433
- const id = await openSelectIdDialog(connection, null, false);
444
+ const { iobrokerHandler } = await import('../common/IobrokerHandler.js');
445
+ const id = await openSelectIdDialog(iobrokerHandler.connection, null, false);
434
446
  if (id) { const inp = this._getDomElement('bdObj'); if (inp) inp.value = id; }
435
447
  } catch (_) {}
436
- });
437
-
438
- this._getDomElement('bdHistoric')?.addEventListener('click', () => {
439
- // historic signals - show simple list if available
440
- });
448
+ };
449
+ this._getDomElement('bdObjBrowse')?.addEventListener('click', browse);
450
+ this._getDomElement('bdObjIOB')?.addEventListener('click', browse);
441
451
 
442
452
  this._getDomElement('bdConvAdd')?.addEventListener('click', () => {
443
453
  const tbody = this._getDomElement('bdConvBody');
@@ -449,9 +459,7 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
449
459
 
450
460
  this._getDomElement('bdConvRemove')?.addEventListener('click', () => {
451
461
  const tbody = this._getDomElement('bdConvBody');
452
- if (!tbody) return;
453
- const last = tbody.querySelector('tr:last-child');
454
- if (last) last.remove();
462
+ tbody?.querySelector('tr:last-child')?.remove();
455
463
  });
456
464
 
457
465
  this._getDomElement('bdOk')?.addEventListener('click', () => {
@@ -463,46 +471,42 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
463
471
  const twoWay = this._getDomElement('bdTwoWay')?.checked ?? false;
464
472
  const invert = this._getDomElement('bdInvert')?.checked ?? false;
465
473
  const type = this._getDomElement('bdType')?.value || 'signal';
466
-
467
- // collect converter rows
468
474
  const converter = [];
469
475
  this._getDomElement('bdConvBody')?.querySelectorAll('tr').forEach(tr => {
470
- const inputs = tr.querySelectorAll('input');
471
- if (inputs[0]?.value || inputs[1]?.value)
472
- converter.push({ condition: inputs[0]?.value || '', value: inputs[1]?.value || '' });
476
+ const ins = tr.querySelectorAll('input');
477
+ if (ins[0]?.value || ins[1]?.value)
478
+ converter.push({ condition: ins[0]?.value || '', value: ins[1]?.value || '' });
473
479
  });
474
480
 
475
- if (uuid && key && signal) {
476
- if (!this._bindings[uuid]) this._bindings[uuid] = {};
477
- this._bindings[uuid][key] = { signal, formula, twoWay, invert, type, writeBack, formulaWB, converter };
478
- this._syncHost();
479
- this._refreshBindRows();
480
- } else if (uuid && key && !signal) {
481
- this._clearBinding({ uuid, key });
481
+ if (uuid && key) {
482
+ if (signal) {
483
+ if (!this._bindings[uuid]) this._bindings[uuid] = {};
484
+ this._bindings[uuid][key] = { signal, formula, twoWay, invert, type, writeBack, formulaWB, converter };
485
+ } else {
486
+ this._clearBinding({ uuid, key });
487
+ }
488
+ this._syncHost(); this._refreshBindRows();
482
489
  }
483
-
484
490
  ov?.classList.remove('open');
485
491
  });
486
492
  }
487
493
 
488
494
  _openBindDialog({ uuid, key } = {}) {
489
495
  if (!uuid || !key) return;
490
- const ex = this._bindings[uuid]?.[key];
491
-
492
- const getEl = id => this._getDomElement(id);
496
+ const ex = this._bindings[uuid]?.[key];
493
497
  const label = this._BINDABLE.find(b => b.key === key)?.label || key;
494
-
495
- getEl('bdPropLabel').textContent = label;
496
- getEl('bdObj').value = ex?.signal || '';
497
- getEl('bdFormula').value = ex?.formula || 'val';
498
- getEl('bdFormulaWB').value = ex?.formulaWB || '';
499
- getEl('bdWriteBack').value = ex?.writeBack || '';
500
- getEl('bdTwoWay').checked = ex?.twoWay || false;
501
- getEl('bdInvert').checked = ex?.invert || false;
502
- if (getEl('bdType')) getEl('bdType').value = ex?.type || 'signal';
503
-
504
- // fill converter
505
- const tbody = getEl('bdConvBody');
498
+ const g = id => this._getDomElement(id);
499
+
500
+ g('bdPropLabel').textContent = label;
501
+ g('bdObj').value = ex?.signal || '';
502
+ g('bdFormula').value = ex?.formula || 'val';
503
+ g('bdFormulaWB').value = ex?.formulaWB || '';
504
+ g('bdWriteBack').value = ex?.writeBack || '';
505
+ g('bdTwoWay').checked = ex?.twoWay || false;
506
+ g('bdInvert').checked = ex?.invert || false;
507
+ if (g('bdType')) g('bdType').value = ex?.type || 'signal';
508
+
509
+ const tbody = g('bdConvBody');
506
510
  if (tbody) {
507
511
  tbody.innerHTML = '';
508
512
  (ex?.converter || []).forEach(row => {
@@ -513,7 +517,7 @@ export class IobrokerWebui3DScreenPropertiesPanel extends BaseCustomWebComponent
513
517
  }
514
518
 
515
519
  this._ctxTarget = { uuid, key };
516
- getEl('bdOverlay')?.classList.add('open');
520
+ g('bdOverlay')?.classList.add('open');
517
521
  }
518
522
 
519
523
  _syncHost() {
@@ -19,6 +19,7 @@ import { CommandHandling } from './CommandHandling.js';
19
19
  import propertiesTypeInfo from "../generated/Properties.json" with { type: 'json' };
20
20
  import "../runtime/controls.js";
21
21
  import "./IobrokerWebuiSolutionExplorer.js";
22
+ import "./IobrokerWebuiWidgetGallery.js";
22
23
  import "./IobrokerWebuiMonacoEditor.js";
23
24
  import "./IobrokerWebuiEventAssignment.js";
24
25
  import "./IobrokerWebuiPropertyGrid.js";
@@ -88,10 +89,14 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
88
89
  <iobroker-webui-solution-explorer id="solutionExplorer"></iobroker-solution-explorer>
89
90
  </div>
90
91
 
91
- <div title="outline" dock-spawn-dock-type="down" dock-spawn-dock-to="treeUpper" dock-spawn-dock-ratio="0.33"
92
+ <div id="outlineDock" title="outline" dock-spawn-dock-type="down" dock-spawn-dock-to="treeUpper" dock-spawn-dock-ratio="0.33"
92
93
  style="overflow: hidden; width: 100%;">
93
94
  <node-projects-tree-view-extended name="tree" id="treeViewExtended"></node-projects-tree-view-extended>
94
95
  </div>
96
+
97
+ <div id="galleryDock" title="gallery" dock-spawn-dock-to="outlineDock" style="overflow: hidden; width: 100%;">
98
+ <iobroker-webui-widget-gallery id="widgetGallery"></iobroker-webui-widget-gallery>
99
+ </div>
95
100
 
96
101
  <div id="attributeDock" title="Properties" dock-spawn-dock-type="right" dock-spawn-dock-ratio="0.2">
97
102
  <node-projects-web-component-designer-property-grid-with-header id="propertyGrid"></node-projects-web-component-designer-property-grid-with-header>
@@ -108,7 +113,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
108
113
  <div id="visibilityDock" title="Visibility" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
109
114
  </div>
110
115
 
111
- <div id="effectsDock" title="Projects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
116
+ <div id="effectsDock" title="Effects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
112
117
  </div>
113
118
 
114
119
  <div id="animationsDock" title="Animations" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
@@ -156,6 +161,8 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
156
161
  this.eventsAssignment = this._getDomElement('eventsList');
157
162
  this.refactorView = this._getDomElement('refactorView');
158
163
  this.translationEditor = this._getDomElement('translationEditor');
164
+ this.widgetGallery = this._getDomElement('widgetGallery');
165
+ this.widgetGallery.serviceContainer = serviceContainer;
159
166
  this.settingsEditor = this._getDomElement('settingsEditor');
160
167
  this.settingsEditor.getTypeInfo = (obj, type) => typeInfoFromJsonSchema(propertiesTypeInfo, obj, type);
161
168
  this.settingsEditor.propertyChanged.on((prp) => {
@@ -313,7 +320,7 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
313
320
 
314
321
  setTimeout(makeSetup('visibilityDock', () => this._setupVisibilityPanel(), 'Visibility'), 1000);
315
322
  setTimeout(makeSetup('animationsDock', () => this._setupAnimationsPanel(), 'Animations'), 1200);
316
- setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Projects'), 1400);
323
+ setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Effects'), 1400);
317
324
  }
318
325
 
319
326
  _setupVisibilityPanel() {
@@ -1430,6 +1437,14 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1430
1437
  cfgList.forEach((cfg, i) => content.appendChild(buildEffectBlock(cfg, i)));
1431
1438
  }
1432
1439
 
1440
+ // shows the live rendered widgets of a tree folder in the gallery panel
1441
+ showWidgetGallery(data, title) {
1442
+ this.widgetGallery?.showForSource(data, title);
1443
+ try {
1444
+ this.activateDockById('galleryDock');
1445
+ }
1446
+ catch (e) { }
1447
+ }
1433
1448
  /* Move to a Dock Spawn Helper */
1434
1449
  activateDockById(name) {
1435
1450
  this.activateDock(this._getDomElement(name));
@@ -2138,44 +2153,35 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2138
2153
 
2139
2154
  _3dPropsActive = false;
2140
2155
  _3dPropsPanel = null;
2141
- _3dSettPanel = null;
2142
2156
  _3dProjPanel = null;
2143
2157
 
2144
2158
  _activate3DPropertiesMode(editor3DEl) {
2145
2159
  if (this._3dPropsActive) return;
2146
2160
  this._3dPropsActive = true;
2147
2161
 
2148
- // Helper: hide existing children and track state
2149
- const hideChildren = (el) => {
2150
- for (const c of el.children) {
2162
+ const attrDock = this._getDomElement('attributeDock');
2163
+ const projDock = this._getDomElement('effectsDock');
2164
+
2165
+ // Hide existing children of attributeDock only
2166
+ if (attrDock) {
2167
+ for (const c of attrDock.children) {
2151
2168
  c.__3d_prev_display = c.style.display;
2152
2169
  c.style.display = 'none';
2153
2170
  }
2154
- };
2155
- // Helper: restore
2156
- const showChildren = (el) => {
2157
- for (const c of el.children) {
2158
- if (c.__3d_prev_display !== undefined) {
2159
- c.style.display = c.__3d_prev_display;
2160
- delete c.__3d_prev_display;
2161
- }
2171
+ }
2172
+ if (projDock) {
2173
+ for (const c of projDock.children) {
2174
+ c.__3d_prev_display = c.style.display;
2175
+ c.style.display = 'none';
2162
2176
  }
2163
- };
2164
-
2165
- const attrDock = this._getDomElement('attributeDock');
2166
- const settiDock = this._getDomElement('settingsDock');
2167
- const projDock = this._getDomElement('effectsDock'); // reuse effectsDock for Project
2168
-
2169
- if (attrDock) hideChildren(attrDock);
2170
- if (settiDock) hideChildren(settiDock);
2171
- if (projDock) hideChildren(projDock);
2177
+ }
2172
2178
 
2173
2179
  const tryConnect = () => {
2174
2180
  if (!editor3DEl._editor) { setTimeout(tryConnect, 200); return; }
2175
2181
  const ed = editor3DEl._editor;
2176
2182
  const base = new URL('../../../3d-editor/js/', import.meta.url).href;
2177
2183
 
2178
- // ── attributeDock: Scene / Object / Geom / Mater ───────────────
2184
+ // ── attributeDock: 3D Bindings panel ──────────────────────────
2179
2185
  if (attrDock) {
2180
2186
  const propPanel = document.createElement('iobroker-webui-3dscreen-properties');
2181
2187
  propPanel.id = '__3d_attr';
@@ -2185,94 +2191,6 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2185
2191
  this._3dPropsPanel = propPanel;
2186
2192
  }
2187
2193
 
2188
- // ── settingsDock: SidebarSettings + Visibility ─────────────────
2189
- if (settiDock) {
2190
- const wrap = document.createElement('div');
2191
- wrap.id = '__3d_setti';
2192
- wrap.style.cssText = 'width:100%;height:100%;overflow:auto;background:#1a1a1a;color:#ccc;font-family:Helvetica,Arial,sans-serif;font-size:12px;';
2193
-
2194
- Promise.all([
2195
- import(/* @vite-ignore */ base + 'Sidebar.Settings.js'),
2196
- ]).then(([{ SidebarSettings }]) => {
2197
- const settPanel = new SidebarSettings(ed);
2198
- settPanel.dom.style.cssText = 'padding:8px;';
2199
- wrap.appendChild(settPanel.dom);
2200
-
2201
- // ── Visibility section ─────────────────────────────────
2202
- const visDiv = document.createElement('div');
2203
- visDiv.style.cssText = 'border-top:1px solid #3c3c3c;margin-top:8px;padding:10px;';
2204
- visDiv.innerHTML = `
2205
- <div style="color:#9cdcfe;font-size:10px;font-weight:bold;text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px;">
2206
- Screen Visibility Control
2207
- </div>
2208
- <label style="display:flex;align-items:center;gap:8px;margin-bottom:8px;cursor:pointer;font-size:11px;">
2209
- <input type="checkbox" id="__3d_vis_en" />
2210
- Enable Visibility Control
2211
- </label>
2212
- <div style="font-size:11px;color:#888;margin-bottom:4px;">Only for groups:</div>
2213
- <div id="__3d_vis_groups" style="border:1px solid #444;padding:6px;max-height:100px;overflow-y:auto;background:#111;font-size:11px;margin-bottom:8px;">
2214
- <span style="color:#555;">Loading...</span>
2215
- </div>
2216
- <div style="font-size:11px;color:#888;margin-bottom:4px;">If not in group:</div>
2217
- <select id="__3d_vis_act" style="width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;margin-bottom:8px;">
2218
- <option value="hide">Show "Access Denied"</option>
2219
- <option value="redirect">Redirect to screen</option>
2220
- </select>
2221
- <div id="__3d_vis_rdiv" style="display:none;">
2222
- <div style="font-size:11px;color:#888;margin-bottom:4px;">Redirect screen:</div>
2223
- <input id="__3d_vis_redir" type="text" style="width:100%;background:#3c3c3c;border:1px solid #555;color:#ccc;padding:4px;font-size:11px;box-sizing:border-box;" />
2224
- </div>
2225
- `;
2226
-
2227
- const settings = editor3DEl.sceneData?.settings ?? {};
2228
- editor3DEl.sceneData = editor3DEl.sceneData ?? {};
2229
- editor3DEl.sceneData.settings = settings;
2230
- if (!settings.visibilityEnabled) settings.visibilityEnabled = false;
2231
- if (!settings.visibilityGroups) settings.visibilityGroups = [];
2232
- if (!settings.visibilityAction) settings.visibilityAction = 'hide';
2233
- if (!settings.visibilityRedirectScreen) settings.visibilityRedirectScreen = '';
2234
-
2235
- const save = () => {};
2236
- const enEl = visDiv.querySelector('#__3d_vis_en');
2237
- const actEl = visDiv.querySelector('#__3d_vis_act');
2238
- const rdDiv = visDiv.querySelector('#__3d_vis_rdiv');
2239
- const rdEl = visDiv.querySelector('#__3d_vis_redir');
2240
-
2241
- if (enEl) { enEl.checked = settings.visibilityEnabled; enEl.addEventListener('change', e => { settings.visibilityEnabled = e.target.checked; }); }
2242
- if (actEl) { actEl.value = settings.visibilityAction; actEl.addEventListener('change', e => { settings.visibilityAction = e.target.value; if (rdDiv) rdDiv.style.display = e.target.value === 'redirect' ? 'block' : 'none'; }); }
2243
- if (rdDiv) rdDiv.style.display = settings.visibilityAction === 'redirect' ? 'block' : 'none';
2244
- if (rdEl) { rdEl.value = settings.visibilityRedirectScreen || ''; rdEl.addEventListener('input', e => { settings.visibilityRedirectScreen = e.target.value; }); }
2245
-
2246
- // Load groups
2247
- const grpEl = visDiv.querySelector('#__3d_vis_groups');
2248
- iobrokerHandler.getUserGroups?.().then(groups => {
2249
- if (!grpEl) return;
2250
- if (!groups?.length) { grpEl.innerHTML = '<span style="color:#555;">No groups</span>'; return; }
2251
- grpEl.innerHTML = '';
2252
- groups.forEach(g => {
2253
- const id = typeof g === 'string' ? g : (g.id ?? g._id ?? g.name ?? g);
2254
- const lbl = typeof g === 'string' ? g : (g.name ?? id);
2255
- const chk = (settings.visibilityGroups || []).includes(id);
2256
- const row = document.createElement('label');
2257
- row.style.cssText = 'display:flex;align-items:center;gap:6px;padding:2px 0;cursor:pointer;';
2258
- row.innerHTML = `<input type="checkbox" ${chk ? 'checked' : ''} />${lbl}`;
2259
- row.querySelector('input').addEventListener('change', ev => {
2260
- const arr = settings.visibilityGroups || [];
2261
- if (ev.target.checked) { if (!arr.includes(id)) arr.push(id); }
2262
- else { const i = arr.indexOf(id); if (i >= 0) arr.splice(i, 1); }
2263
- settings.visibilityGroups = arr;
2264
- });
2265
- grpEl.appendChild(row);
2266
- });
2267
- }).catch(() => { if (grpEl) grpEl.innerHTML = '<span style="color:#555;">—</span>'; });
2268
-
2269
- wrap.appendChild(visDiv);
2270
- });
2271
-
2272
- settiDock.appendChild(wrap);
2273
- this._3dSettPanel = wrap;
2274
- }
2275
-
2276
2194
  // ── effectsDock: SidebarProject (Geometries / Materials / Textures) ──
2277
2195
  if (projDock) {
2278
2196
  const wrap = document.createElement('div');
@@ -2307,12 +2225,10 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
2307
2225
  }
2308
2226
  };
2309
2227
 
2310
- restore('attributeDock', this._3dPropsPanel);
2311
- restore('settingsDock', this._3dSettPanel);
2312
- restore('effectsDock', this._3dProjPanel);
2228
+ restore('attributeDock', this._3dPropsPanel);
2229
+ restore('effectsDock', this._3dProjPanel);
2313
2230
 
2314
2231
  this._3dPropsPanel = null;
2315
- this._3dSettPanel = null;
2316
2232
  this._3dProjPanel = null;
2317
2233
  }
2318
2234
  }