iobroker.mywebui 1.37.75 → 1.37.77

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/io-package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "mywebui",
4
- "version": "1.37.75",
4
+ "version": "1.37.77",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.37.75",
3
+ "version": "1.37.77",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -74,20 +74,20 @@
74
74
  "@alcalzone/release-script-plugin-iobroker": "^4.0.0",
75
75
  "@alcalzone/release-script-plugin-license": "^4.0.0",
76
76
  "@blockly/zoom-to-fit": "^7.0.3",
77
- "@iobroker/socket-client": "^5.0.2",
78
- "@iobroker/testing": "^5.1.1",
79
- "@iobroker/webcomponent-selectid-dialog": "^1.0.12",
80
77
  "@gokturk413/base-custom-webcomponent": "*",
81
- "@node-projects/lean-he-esm": "^3.4.1",
82
- "@node-projects/node-html-parser-esm": "^6.4.1",
83
78
  "@gokturk413/propertygrid.webcomponent": "*",
84
79
  "@gokturk413/splitview.webcomponent": "^1.0.1",
85
80
  "@gokturk413/web-component-designer": "*",
86
81
  "@gokturk413/web-component-designer-codeview-monaco": "*",
87
82
  "@gokturk413/web-component-designer-htmlparserservice-nodehtmlparser": "*",
88
- "@node-projects/web-component-designer-stylesheetservice-css-tools": "^0.1.11",
89
83
  "@gokturk413/web-component-designer-visualization-addons": "*",
90
84
  "@gokturk413/web-component-designer-widgets-wunderbaum": "*",
85
+ "@iobroker/socket-client": "^5.0.2",
86
+ "@iobroker/testing": "^5.1.1",
87
+ "@iobroker/webcomponent-selectid-dialog": "^1.0.12",
88
+ "@node-projects/lean-he-esm": "^3.4.1",
89
+ "@node-projects/node-html-parser-esm": "^6.4.1",
90
+ "@node-projects/web-component-designer-stylesheetservice-css-tools": "^0.1.11",
91
91
  "@types/json-schema": "^7.0.15",
92
92
  "@web/dev-server": "^0.4.6",
93
93
  "blockly": "^12.3.1",
@@ -107,10 +107,12 @@
107
107
  "gulp-replace": "^1.1.4",
108
108
  "javascript-obfuscator": "^5.3.0",
109
109
  "long": "^5.3.2",
110
+ "marked": "^18.0.3",
110
111
  "mobile-drag-drop": "^3.0.0-rc.0",
111
112
  "mocha": "^11.7.4",
112
113
  "monaco-editor": "^0.50.0",
113
114
  "nyc": "^17.1.0",
115
+ "puppeteer": "^24.42.0",
114
116
  "sinon-chai": "^4.0.1",
115
117
  "toastify-js": "^1.12.0",
116
118
  "ts-node": "^10.9.2",
@@ -1115,154 +1115,192 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1115
1115
  const designItem = items[0];
1116
1116
  const element = designItem.element;
1117
1117
 
1118
- let cfg = {};
1119
- try { cfg = JSON.parse(element.getAttribute('data-effects') || '{}'); } catch (e) {}
1120
-
1121
- const collectEffectsCfg = () => {
1122
- const out = {};
1123
- if (typeSel.value) out.type = typeSel.value;
1124
- out.trigger = triggerSel.value || 'load';
1125
- out.duration = parseFloat(durationInp.value) || 0.5;
1126
- if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1127
- if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1128
- out.ease = easeSel.value || 'power2.out';
1129
- if (triggerSel.value === 'oid') {
1130
- out.condition = condSel.value;
1131
- out.conditionValue = condValInp.value;
1132
- }
1133
- if (typeSel.value === 'glow') {
1134
- out.glowColor = glowColorInp.value;
1135
- out.glowSize = parseInt(glowSizeInp.value) || 10;
1136
- }
1137
- if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1138
- // Preserve all _bind properties
1139
- for (const [k, v] of Object.entries(cfg)) {
1140
- if (k.endsWith('_bind') && v) out[k] = v;
1141
- }
1142
- return out;
1118
+ let raw = null;
1119
+ try { raw = JSON.parse(element.getAttribute('data-effects') || 'null'); } catch (e) {}
1120
+ let cfgList = Array.isArray(raw) ? raw : (raw ? [raw] : []);
1121
+
1122
+ content.innerHTML = '';
1123
+
1124
+ const saveAll = () => {
1125
+ const data = cfgList.filter(c => c._collect).map(c => c._collect()).filter(c => c.type);
1126
+ if (data.length === 0) designItem.removeAttribute('data-effects');
1127
+ else if (data.length === 1) designItem.setAttribute('data-effects', JSON.stringify(data[0]));
1128
+ else designItem.setAttribute('data-effects', JSON.stringify(data));
1143
1129
  };
1130
+ const saveAndRefresh = () => { saveAll(); this._updateEffectsPanel(); };
1144
1131
 
1145
- const save = () => {
1146
- const c = collectEffectsCfg();
1147
- if (!c.type) designItem.removeAttribute('data-effects');
1148
- else designItem.setAttribute('data-effects', JSON.stringify(c));
1132
+ // ── Add Effect button ─────────────────────────────────────────────────
1133
+ const addBtn = document.createElement('button');
1134
+ addBtn.textContent = '+ Add Effect';
1135
+ addBtn.style.cssText = 'width:100%;font-size:11px;padding:4px;cursor:pointer;border:1px solid #4caf50;border-radius:3px;background:#e8f5e9;color:#2e7d32;margin-bottom:8px;';
1136
+ addBtn.onclick = () => {
1137
+ const existing = cfgList.filter(c => c._collect).map(c => c._collect()).filter(c => c.type);
1138
+ existing.push({ type: 'fadeIn', trigger: 'load', duration: 0.5, ease: 'power2.out' });
1139
+ const val = existing.length === 1 ? existing[0] : existing;
1140
+ designItem.setAttribute('data-effects', JSON.stringify(val));
1141
+ this._updateEffectsPanel();
1149
1142
  };
1143
+ content.appendChild(addBtn);
1150
1144
 
1151
- const saveAndRefresh = () => { save(); this._updateEffectsPanel(); };
1145
+ if (cfgList.length === 0) {
1146
+ const ph = document.createElement('p');
1147
+ ph.style.cssText = 'color:#999;font-style:italic;font-size:11px;text-align:center;margin-top:12px;';
1148
+ ph.textContent = 'No effects. Click "+ Add Effect".';
1149
+ content.appendChild(ph);
1150
+ return;
1151
+ }
1152
1152
 
1153
- const field = (label, propKey, inputEl) => {
1154
- const row = document.createElement('div');
1155
- row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
1156
- if (propKey) {
1157
- row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-effects', saveAndRefresh));
1158
- } else {
1159
- const sp = document.createElement('div');
1160
- sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
1161
- row.appendChild(sp);
1162
- }
1163
- const lbl = document.createElement('span');
1164
- lbl.textContent = label;
1165
- lbl.style.cssText = 'min-width:78px;font-size:11px;color:#555;';
1166
- row.appendChild(lbl); row.appendChild(inputEl);
1167
- return row;
1168
- };
1153
+ // ── Build one collapsible block per effect ────────────────────────────
1154
+ const buildEffectBlock = (cfg, index) => {
1155
+ const wrapper = document.createElement('div');
1156
+ wrapper.style.cssText = 'border:1px solid #ddd;border-radius:4px;margin-bottom:6px;overflow:hidden;';
1157
+
1158
+ // Header
1159
+ const header = document.createElement('div');
1160
+ header.style.cssText = 'display:flex;align-items:center;justify-content:space-between;background:#f0f4ff;padding:4px 6px;cursor:pointer;user-select:none;';
1161
+ const headerLeft = document.createElement('span');
1162
+ headerLeft.style.cssText = 'font-size:11px;font-weight:600;color:#1a237e;';
1163
+ headerLeft.textContent = `▶ #${index + 1} — ${cfg.type || 'none'} (${cfg.trigger || 'load'})`;
1164
+ const delBtn = document.createElement('button');
1165
+ delBtn.textContent = '';
1166
+ delBtn.title = 'Remove this effect';
1167
+ delBtn.style.cssText = 'font-size:10px;padding:1px 5px;cursor:pointer;border:1px solid #e57373;border-radius:3px;background:#ffebee;color:#c62828;';
1168
+ delBtn.onclick = (e) => {
1169
+ e.stopPropagation();
1170
+ const data = cfgList.filter(c => c._collect).map(c => c._collect()).filter(c => c.type);
1171
+ data.splice(index, 1);
1172
+ if (data.length === 0) designItem.removeAttribute('data-effects');
1173
+ else if (data.length === 1) designItem.setAttribute('data-effects', JSON.stringify(data[0]));
1174
+ else designItem.setAttribute('data-effects', JSON.stringify(data));
1175
+ this._updateEffectsPanel();
1176
+ };
1177
+ header.appendChild(headerLeft);
1178
+ header.appendChild(delBtn);
1169
1179
 
1170
- const inp = (val, type = 'text') => {
1171
- const i = document.createElement('input');
1172
- i.type = type; i.value = val ?? '';
1173
- i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1174
- i.onchange = save; return i;
1175
- };
1180
+ // Body (collapsed by default)
1181
+ const body = document.createElement('div');
1182
+ body.style.cssText = 'padding:8px;display:none;';
1183
+ header.onclick = () => {
1184
+ body.style.display = body.style.display === 'none' ? '' : 'none';
1185
+ headerLeft.textContent = (body.style.display === 'none' ? '▶' : '▼') + ` #${index + 1} — ${cfg.type || 'none'} (${cfg.trigger || 'load'})`;
1186
+ };
1176
1187
 
1177
- const sel = (options, val) => {
1178
- const s = document.createElement('select');
1179
- s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1180
- options.forEach(([v, t]) => {
1181
- const o = document.createElement('option');
1182
- o.value = v; o.textContent = t; o.selected = v === val;
1183
- s.appendChild(o);
1184
- });
1185
- s.onchange = save; return s;
1186
- };
1188
+ // ── helpers scoped to this block ──────────────────────────────────
1189
+ const makeBindSq = (propKey) => this._makeBindSquare(propKey, cfg, designItem, 'data-effects', saveAndRefresh);
1190
+
1191
+ const field = (label, propKey, inputEl) => {
1192
+ const row = document.createElement('div');
1193
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
1194
+ row.appendChild(propKey ? makeBindSq(propKey) : (() => { const sp = document.createElement('div'); sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;'; return sp; })());
1195
+ const lbl = document.createElement('span');
1196
+ lbl.textContent = label;
1197
+ lbl.style.cssText = 'min-width:78px;font-size:11px;color:#555;';
1198
+ row.appendChild(lbl); row.appendChild(inputEl);
1199
+ return row;
1200
+ };
1187
1201
 
1188
- const typeSel = sel([
1189
- ['','— none —'],['fadeIn','Fade In'],['fadeOut','Fade Out'],
1190
- ['slideInLeft','Slide In Left'],['slideInRight','Slide In Right'],
1191
- ['slideInTop','Slide In Top'],['slideInBottom','Slide In Bottom'],
1192
- ['bounce','Bounce'],['pulse','Pulse'],['shake','Shake'],
1193
- ['glow','Glow'],['blur','Blur'],['spin','Spin'],['flip','Flip 3D']
1194
- ], cfg.type || '');
1195
-
1196
- const triggerSel = sel([
1197
- ['load','On Load'],['hover','On Hover'],['click','On Click'],['oid','OID State']
1198
- ], cfg.trigger || 'load');
1199
-
1200
- const durationInp = inp(cfg.duration ?? 0.5, 'number');
1201
- const delayInp = inp(cfg.delay ?? 0, 'number');
1202
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
1203
-
1204
- const easeSel = sel([
1205
- ['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
1206
- ['power1.inOut','power1.inOut'],['bounce.out','bounce.out'],
1207
- ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],['none','none']
1208
- ], cfg.ease || 'power2.out');
1209
-
1210
- const condSel = sel([['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']], cfg.condition || 'equal');
1211
- const condValInp = inp(cfg.conditionValue ?? 'true');
1212
- const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
1213
- const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1214
- const blurInp = inp(cfg.blurAmount || 5, 'number');
1215
-
1216
- // ── Build UI ──────────────────────────────────────────────────────────
1217
- content.innerHTML = '';
1202
+ const inp = (val, type = 'text') => {
1203
+ const i = document.createElement('input');
1204
+ i.type = type; i.value = val ?? '';
1205
+ i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1206
+ i.onchange = saveAll; return i;
1207
+ };
1208
+
1209
+ const sel = (options, val) => {
1210
+ const s = document.createElement('select');
1211
+ s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1212
+ options.forEach(([v, t]) => { const o = document.createElement('option'); o.value = v; o.textContent = t; o.selected = v === val; s.appendChild(o); });
1213
+ s.onchange = saveAll; return s;
1214
+ };
1215
+
1216
+ const typeSel = sel([
1217
+ ['','— none —'],['fadeIn','Fade In'],['fadeOut','Fade Out'],
1218
+ ['slideInLeft','Slide In Left'],['slideInRight','Slide In Right'],
1219
+ ['slideInTop','Slide In Top'],['slideInBottom','Slide In Bottom'],
1220
+ ['bounce','Bounce'],['pulse','Pulse'],['shake','Shake'],
1221
+ ['glow','Glow'],['blur','Blur'],['spin','Spin'],['flip','Flip 3D']
1222
+ ], cfg.type || '');
1223
+
1224
+ const triggerSel = sel([
1225
+ ['load','On Load'],['hover','On Hover'],['click','On Click'],['oid','OID State']
1226
+ ], cfg.trigger || 'load');
1227
+
1228
+ const durationInp = inp(cfg.duration ?? 0.5, 'number');
1229
+ const delayInp = inp(cfg.delay ?? 0, 'number');
1230
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
1231
+ const easeSel = sel([
1232
+ ['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
1233
+ ['power1.inOut','power1.inOut'],['bounce.out','bounce.out'],
1234
+ ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],['none','none']
1235
+ ], cfg.ease || 'power2.out');
1236
+
1237
+ const condSel = sel([['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']], cfg.condition || 'equal');
1238
+ const condValInp = inp(cfg.conditionValue ?? 'true');
1239
+ const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
1240
+ const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1241
+ const blurInp = inp(cfg.blurAmount || 5, 'number');
1242
+
1243
+ body.appendChild(field('Type', 'type', typeSel));
1244
+ body.appendChild(field('Trigger', null, triggerSel));
1245
+ body.appendChild(field('Duration (s)', 'duration', durationInp));
1246
+ body.appendChild(field('Delay (s)', 'delay', delayInp));
1247
+ body.appendChild(field('Repeat', 'repeat', repeatInp));
1248
+ body.appendChild(field('Ease', 'ease', easeSel));
1249
+
1250
+ // OID section
1251
+ const oidSection = document.createElement('div');
1252
+ oidSection.style.display = cfg.trigger === 'oid' ? '' : 'none';
1253
+ const oidLabel = document.createElement('div');
1254
+ oidLabel.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#555;margin-bottom:3px;padding-left:4px;';
1255
+ oidLabel.appendChild(makeBindSq('oid'));
1256
+ const oidLabelText = document.createElement('span'); oidLabelText.textContent = 'OID'; oidLabel.appendChild(oidLabelText);
1257
+ const condRowDiv = document.createElement('div');
1258
+ condRowDiv.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;';
1259
+ condRowDiv.appendChild(makeBindSq('condition')); condRowDiv.appendChild(condSel);
1260
+ const condValRowDiv = document.createElement('div');
1261
+ condValRowDiv.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;padding-left:4px;';
1262
+ condValRowDiv.appendChild(makeBindSq('conditionValue')); condValRowDiv.appendChild(condValInp);
1263
+ oidSection.appendChild(oidLabel); oidSection.appendChild(condRowDiv); oidSection.appendChild(condValRowDiv);
1264
+ body.appendChild(oidSection);
1265
+ triggerSel.addEventListener('change', () => { oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none'; });
1266
+
1267
+ // Glow section
1268
+ const glowSection = document.createElement('div');
1269
+ glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
1270
+ glowSection.appendChild(field('Glow Color', 'glowColor', glowColorInp));
1271
+ glowSection.appendChild(field('Glow Size', 'glowSize', glowSizeInp));
1272
+ body.appendChild(glowSection);
1273
+ typeSel.addEventListener('change', () => { glowSection.style.display = typeSel.value === 'glow' ? '' : 'none'; });
1274
+
1275
+ // Blur section
1276
+ const blurSection = document.createElement('div');
1277
+ blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
1278
+ blurSection.appendChild(field('Blur (px)', 'blurAmount', blurInp));
1279
+ body.appendChild(blurSection);
1280
+ typeSel.addEventListener('change', () => { blurSection.style.display = typeSel.value === 'blur' ? '' : 'none'; });
1281
+
1282
+ // _collect: read current UI values back into a plain object
1283
+ cfg._collect = () => {
1284
+ const out = {};
1285
+ if (typeSel.value) out.type = typeSel.value;
1286
+ out.trigger = triggerSel.value || 'load';
1287
+ out.duration = parseFloat(durationInp.value) || 0.5;
1288
+ if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1289
+ if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1290
+ out.ease = easeSel.value || 'power2.out';
1291
+ if (triggerSel.value === 'oid') { out.condition = condSel.value; out.conditionValue = condValInp.value; }
1292
+ if (typeSel.value === 'glow') { out.glowColor = glowColorInp.value; out.glowSize = parseInt(glowSizeInp.value) || 10; }
1293
+ if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1294
+ for (const [k, v] of Object.entries(cfg)) { if (k.endsWith('_bind') && v) out[k] = v; }
1295
+ return out;
1296
+ };
1297
+
1298
+ wrapper.appendChild(header);
1299
+ wrapper.appendChild(body);
1300
+ return wrapper;
1301
+ };
1218
1302
 
1219
- const clearBtn = document.createElement('button');
1220
- clearBtn.textContent = 'Clear';
1221
- clearBtn.style.cssText = 'float:right;font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;margin-bottom:8px;';
1222
- clearBtn.onclick = () => { designItem.removeAttribute('data-effects'); this._updateEffectsPanel(); };
1223
- content.appendChild(clearBtn);
1224
-
1225
- content.appendChild(field('Type', 'type', typeSel));
1226
- content.appendChild(field('Trigger', null, triggerSel));
1227
- content.appendChild(field('Duration (s)', 'duration', durationInp));
1228
- content.appendChild(field('Delay (s)', 'delay', delayInp));
1229
- content.appendChild(field('Repeat', 'repeat', repeatInp));
1230
- content.appendChild(field('Ease', 'ease', easeSel));
1231
-
1232
- const oidSection = document.createElement('div');
1233
- oidSection.style.display = cfg.trigger === 'oid' ? '' : 'none';
1234
- const oidLabel = document.createElement('div');
1235
- oidLabel.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#555;margin-bottom:3px;padding-left:4px;';
1236
- oidLabel.appendChild(this._makeBindSquare('oid', cfg, designItem, 'data-effects', saveAndRefresh));
1237
- const oidLabelText = document.createElement('span');
1238
- oidLabelText.textContent = 'OID';
1239
- oidLabel.appendChild(oidLabelText);
1240
- const condRowDiv = document.createElement('div');
1241
- condRowDiv.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;';
1242
- condRowDiv.appendChild(this._makeBindSquare('condition', cfg, designItem, 'data-effects', saveAndRefresh));
1243
- condRowDiv.appendChild(condSel);
1244
- const condValRowDiv = document.createElement('div');
1245
- condValRowDiv.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;padding-left:4px;';
1246
- condValRowDiv.appendChild(this._makeBindSquare('conditionValue', cfg, designItem, 'data-effects', saveAndRefresh));
1247
- condValRowDiv.appendChild(condValInp);
1248
- oidSection.appendChild(oidLabel);
1249
- oidSection.appendChild(condRowDiv);
1250
- oidSection.appendChild(condValRowDiv);
1251
- content.appendChild(oidSection);
1252
- triggerSel.addEventListener('change', () => { oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none'; });
1253
-
1254
- const glowSection = document.createElement('div');
1255
- glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
1256
- glowSection.appendChild(field('Glow Color', 'glowColor', glowColorInp));
1257
- glowSection.appendChild(field('Glow Size', 'glowSize', glowSizeInp));
1258
- content.appendChild(glowSection);
1259
- typeSel.addEventListener('change', () => { glowSection.style.display = typeSel.value === 'glow' ? '' : 'none'; });
1260
-
1261
- const blurSection = document.createElement('div');
1262
- blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
1263
- blurSection.appendChild(field('Blur (px)', 'blurAmount', blurInp));
1264
- content.appendChild(blurSection);
1265
- typeSel.addEventListener('change', () => { blurSection.style.display = typeSel.value === 'blur' ? '' : 'none'; });
1303
+ cfgList.forEach((cfg, i) => content.appendChild(buildEffectBlock(cfg, i)));
1266
1304
  }
1267
1305
 
1268
1306
  /* Move to a Dock Spawn Helper */
@@ -489,11 +489,17 @@ export async function scanAndApplyEffects(root) {
489
489
  const elements = (root || document).querySelectorAll('[data-effects]');
490
490
  for (const el of elements) {
491
491
  try {
492
- const cfg = JSON.parse(el.getAttribute('data-effects'));
493
- const existing = _activeEffects.get(el);
494
- if (existing?.cleanup) existing.cleanup();
495
- const cleanup = await _applyEffect(el, cfg);
496
- _activeEffects.set(el, { cleanup });
492
+ const raw = JSON.parse(el.getAttribute('data-effects'));
493
+ const cfgList = Array.isArray(raw) ? raw : [raw];
494
+ const existing = _activeEffects.get(el) || [];
495
+ for (const inst of existing) if (inst?.cleanup) inst.cleanup();
496
+ const instances = [];
497
+ for (const cfg of cfgList) {
498
+ if (!cfg || typeof cfg !== 'object') continue;
499
+ const cleanup = await _applyEffect(el, cfg);
500
+ instances.push({ cleanup });
501
+ }
502
+ _activeEffects.set(el, instances);
497
503
  } catch (e) {
498
504
  console.warn('[AnimationService] data-effects parse error on element:', el, e);
499
505
  }
@@ -503,8 +509,9 @@ export async function scanAndApplyEffects(root) {
503
509
  export function cleanupEffects(root) {
504
510
  const elements = (root || document).querySelectorAll('[data-effects]');
505
511
  for (const el of elements) {
506
- const inst = _activeEffects.get(el);
507
- if (inst?.cleanup) { inst.cleanup(); _activeEffects.delete(el); }
512
+ const instances = _activeEffects.get(el) || [];
513
+ for (const inst of instances) if (inst?.cleanup) inst.cleanup();
514
+ _activeEffects.delete(el);
508
515
  }
509
516
  }
510
517
 
package/www/index.html CHANGED
@@ -37,7 +37,6 @@
37
37
  }
38
38
  </script>
39
39
  <script src="./node_modules/es-module-shims/dist/es-module-shims.js"></script>
40
- <script src="/mywebui.0.widgets/importmap.js"></script>
41
40
  <script src="./dist/importmaps/importmap-config.js"></script>
42
41
 
43
42
  <script src="config.js"></script>
package/www/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "iobrober-webui",
3
3
  "short_name": "webui",
4
4
  "description": "",
5
- "start_url": "/mywebui/runtime.html",
5
+ "start_url": "./runtime.html",
6
6
  "display": "standalone"
7
7
  }
8
8
 
@@ -698,6 +698,7 @@ export class BindingsHelper {
698
698
  let valuesObject = new Array(signals.length);
699
699
  for (let i = 0; i < signals.length; i++) {
700
700
  const s = signals[i];
701
+ if (s == null) continue; // ?prop resolved to undefined — skip, re-apply fires when property changes
701
702
  if (s[0] === '?') {
702
703
  if (root) {
703
704
  const nm = s.substring(1);
package/www/runtime.html CHANGED
@@ -30,7 +30,6 @@
30
30
  }
31
31
  </script>
32
32
  <script src="./node_modules/es-module-shims/dist/es-module-shims.js"></script>
33
- <script src="/mywebui.0/widgets/importmap.js"></script>
34
33
  <script src="./dist/importmaps/importmap-runtime.js"></script>
35
34
 
36
35
  <script src="config.js"></script>