iobroker.mywebui 1.37.63 → 1.37.64

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.60",
4
+ "version": "1.37.64",
5
5
  "titleLang": {
6
6
  "en": "mywebui",
7
7
  "de": "mywebui",
@@ -337,7 +337,7 @@
337
337
  "color": "#c8ffe1",
338
338
  "order": 1
339
339
  },
340
- "installedFrom": "iobroker.mywebui@1.37.16"
340
+ "installedFrom": "iobroker.mywebui"
341
341
  },
342
342
  "native": {
343
343
  "licenseKey": "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.mywebui",
3
- "version": "1.37.63",
3
+ "version": "1.37.64",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -651,6 +651,73 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
651
651
  content.appendChild(actionDiv);
652
652
  }
653
653
 
654
+ // ─── Shared binding square helper ────────────────────────────────────────
655
+
656
+ _makeBindSquare(propKey, cfg, designItem, attrName, saveAndRefresh) {
657
+ const bindCfg = cfg[propKey + '_bind'] || null;
658
+ const signal = bindCfg?.signal || null;
659
+
660
+ const btn = document.createElement('div');
661
+ btn.title = signal ? `Bound: ${signal}\n(right-click: edit/clear)` : `Click to bind to ioBroker state`;
662
+ btn.style.cssText = 'width:11px;height:11px;min-width:11px;border:1px solid #596c7a;cursor:pointer;flex-shrink:0;box-sizing:border-box;'
663
+ + (signal ? 'background:orange;' : 'background:transparent;');
664
+
665
+ const openEditor = () => {
666
+ const existingBinding = signal ? { bindableObjectNames: [signal], expression: bindCfg.expression || '', target: 'property' } : null;
667
+ const dynEdt = new IobrokerWebuiBindingsEditor(
668
+ { name: propKey, propertyType: 'propertyAndAttribute' },
669
+ existingBinding, 'property', serviceContainer,
670
+ designItem.instanceServiceContainer, this
671
+ );
672
+ const cw = new IobrokerWebuiConfirmationWrapper();
673
+ cw.title = `Bind "${propKey}"`;
674
+ cw.appendChild(dynEdt);
675
+ const dlg = this.openDialog(cw, { x: 200, y: 200, width: 700, height: 460 });
676
+ cw.cancelClicked.on(() => dlg.close());
677
+ cw.okClicked.on(() => {
678
+ dlg.close();
679
+ const newSignal = dynEdt.objectNames;
680
+ if (newSignal) {
681
+ const bnd = { signal: newSignal };
682
+ if (dynEdt.expression) bnd.expression = dynEdt.expression;
683
+ cfg[propKey + '_bind'] = bnd;
684
+ } else {
685
+ delete cfg[propKey + '_bind'];
686
+ }
687
+ saveAndRefresh();
688
+ });
689
+ };
690
+
691
+ btn.onclick = openEditor;
692
+
693
+ btn.oncontextmenu = (e) => {
694
+ e.preventDefault();
695
+ const existing = document.getElementById('__animbind-ctx');
696
+ if (existing) existing.remove();
697
+ const menu = document.createElement('div');
698
+ menu.id = '__animbind-ctx';
699
+ menu.style.cssText = `position:fixed;left:${e.clientX}px;top:${e.clientY}px;background:#2d2d2d;color:#ddd;font-size:12px;border:1px solid #555;border-radius:3px;z-index:99999;min-width:120px;box-shadow:2px 2px 6px rgba(0,0,0,0.5);`;
700
+ const addItem = (text, cb, disabled = false) => {
701
+ const item = document.createElement('div');
702
+ item.textContent = text;
703
+ item.style.cssText = `padding:6px 12px;cursor:${disabled ? 'default' : 'pointer'};color:${disabled ? '#666' : '#ddd'};`;
704
+ if (!disabled) {
705
+ item.onmouseenter = () => item.style.background = '#3e6db4';
706
+ item.onmouseleave = () => item.style.background = '';
707
+ item.onclick = () => { menu.remove(); cb(); };
708
+ }
709
+ menu.appendChild(item);
710
+ };
711
+ addItem('edit binding', openEditor);
712
+ addItem('clear binding', () => { delete cfg[propKey + '_bind']; saveAndRefresh(); }, !signal);
713
+ document.body.appendChild(menu);
714
+ const close = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener('mousedown', close); } };
715
+ setTimeout(() => document.addEventListener('mousedown', close), 0);
716
+ };
717
+
718
+ return btn;
719
+ }
720
+
654
721
  // ─── Animations Panel ────────────────────────────────────────────────────
655
722
 
656
723
  _setupAnimationsPanel() {
@@ -697,33 +764,78 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
697
764
  try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
698
765
  const controls = cfg.controls || {};
699
766
 
767
+ const collectAnimCfg = () => {
768
+ const e = effectSel.value;
769
+ const c = {};
770
+ const addCtrl = (key, row) => { const v = row._getCtrl(); if (v.oid) c[key] = v; };
771
+ addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
772
+ addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
773
+ addCtrl('reverse', reverseCtrl);
774
+ const out = {};
775
+ if (e) out.effect = e;
776
+ if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
777
+ if (valueFromInp.value) out.valueFrom = valueFromInp.value;
778
+ if (valueToInp.value) out.valueTo = valueToInp.value;
779
+ out.duration = parseFloat(durationInp.value) || 1;
780
+ out.ease = easeSel.value || 'power1.inOut';
781
+ out.repeat = parseInt(repeatInp.value) || 0;
782
+ if (yoyoChk.querySelector('input').checked) out.yoyo = true;
783
+ if (['rotation','scale'].includes(e)) {
784
+ out.transformOriginX = originXInp.value;
785
+ out.transformOriginY = originYInp.value;
786
+ }
787
+ if (e === 'fill' || e === 'svg') {
788
+ if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
789
+ if (fillToInp.value) out.fillColorTo = fillToInp.value;
790
+ }
791
+ if (e === 'motionPath') {
792
+ if (pathIdInp.value) out.pathId = pathIdInp.value;
793
+ if (alignToPathChk.querySelector('input').checked) out.alignToPath = true;
794
+ if (orientToPathChk.querySelector('input').checked) out.orientToPath = true;
795
+ }
796
+ if (Object.keys(c).length) out.controls = c;
797
+ // Preserve all _bind properties from cfg
798
+ for (const [k, v] of Object.entries(cfg)) {
799
+ if (k.endsWith('_bind') && v) out[k] = v;
800
+ }
801
+ return out;
802
+ };
803
+
700
804
  const save = () => {
701
805
  const newCfg = collectAnimCfg();
702
- if (Object.keys(newCfg).length === 0 || (!newCfg.effect && !newCfg.controls)) {
806
+ if (!newCfg.effect && !newCfg.controls && !Object.keys(newCfg).some(k => k.endsWith('_bind'))) {
703
807
  designItem.removeAttribute('data-animation');
704
808
  } else {
705
809
  designItem.setAttribute('data-animation', JSON.stringify(newCfg));
706
810
  }
707
811
  };
708
812
 
709
- const field = (label, inputEl) => {
813
+ const saveAndRefresh = () => { save(); this._updateAnimationsPanel(); };
814
+
815
+ // field(label, propKey, inputEl) — propKey=null → no binding square
816
+ const field = (label, propKey, inputEl) => {
710
817
  const row = document.createElement('div');
711
- row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
818
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
819
+ if (propKey) {
820
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
821
+ } else {
822
+ const sp = document.createElement('div');
823
+ sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
824
+ row.appendChild(sp);
825
+ }
712
826
  const lbl = document.createElement('span');
713
827
  lbl.textContent = label;
714
- lbl.style.cssText = 'min-width:90px;font-size:11px;color:#555;';
828
+ lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
715
829
  row.appendChild(lbl);
716
830
  row.appendChild(inputEl);
717
831
  return row;
718
832
  };
719
833
 
720
- const inp = (val, type = 'text', width = '100%') => {
834
+ const inp = (val, type = 'text') => {
721
835
  const i = document.createElement('input');
722
- i.type = type;
723
- i.value = val ?? '';
724
- i.style.cssText = `flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;width:${width};`;
725
- i.onchange = save;
726
- return i;
836
+ i.type = type; i.value = val ?? '';
837
+ i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
838
+ i.onchange = save; return i;
727
839
  };
728
840
 
729
841
  const sel = (options, val) => {
@@ -734,37 +846,36 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
734
846
  o.value = v; o.textContent = t; o.selected = v === val;
735
847
  s.appendChild(o);
736
848
  });
737
- s.onchange = save;
738
- return s;
849
+ s.onchange = save; return s;
739
850
  };
740
851
 
741
- const chk = (val, label) => {
742
- const wrap = document.createElement('label');
743
- wrap.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
852
+ const chkField = (propKey, label, val) => {
853
+ const row = document.createElement('div');
854
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
855
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
856
+ const lbl = document.createElement('label');
857
+ lbl.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
744
858
  const c = document.createElement('input');
745
859
  c.type = 'checkbox'; c.checked = val === true || val === 'true';
746
860
  c.onchange = save;
747
- wrap.appendChild(c);
748
- wrap.appendChild(document.createTextNode(label));
749
- return wrap;
861
+ lbl.appendChild(c);
862
+ lbl.appendChild(document.createTextNode(label));
863
+ row.appendChild(lbl);
864
+ return row;
750
865
  };
751
866
 
752
- const oidRow = (label, ctrlKey, ctrlCfg) => {
867
+ const oidRow = (label, ctrlCfg) => {
753
868
  const wrap = document.createElement('div');
754
869
  wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
755
-
756
870
  const head = document.createElement('div');
757
871
  head.style.cssText = 'font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
758
872
  head.textContent = label;
759
873
  wrap.appendChild(head);
760
-
761
874
  const oidInp = document.createElement('input');
762
- oidInp.type = 'text';
763
- oidInp.value = ctrlCfg.oid || '';
875
+ oidInp.type = 'text'; oidInp.value = ctrlCfg.oid || '';
764
876
  oidInp.placeholder = 'OID…';
765
877
  oidInp.style.cssText = 'width:100%;box-sizing:border-box;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;margin-bottom:3px;';
766
878
  oidInp.onchange = save;
767
-
768
879
  const pickBtn = document.createElement('button');
769
880
  pickBtn.textContent = '…';
770
881
  pickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;margin-left:3px;';
@@ -772,51 +883,39 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
772
883
  const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
773
884
  if (picked) { oidInp.value = picked; save(); }
774
885
  };
775
-
776
886
  const oidRow2 = document.createElement('div');
777
887
  oidRow2.style.cssText = 'display:flex;margin-bottom:3px;';
778
- oidRow2.appendChild(oidInp);
779
- oidRow2.appendChild(pickBtn);
888
+ oidRow2.appendChild(oidInp); oidRow2.appendChild(pickBtn);
780
889
  wrap.appendChild(oidRow2);
781
-
782
890
  const condSel = sel([
783
- ['equal','='], ['not_equal','≠'], ['less_than','<'], ['less_equal','≤'],
784
- ['greater_than','>'], ['greater_equal','≥'], ['exists','exists']
891
+ ['equal','='],['not_equal','≠'],['less_than','<'],['less_equal','≤'],
892
+ ['greater_than','>'],['greater_equal','≥'],['exists','exists']
785
893
  ], ctrlCfg.condition || 'equal');
786
894
  condSel.style.cssText += 'width:70px;flex:none;margin-right:4px;';
787
895
  const valInp = document.createElement('input');
788
896
  valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
789
- valInp.placeholder = 'value';
790
897
  valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
791
898
  condSel.onchange = save; valInp.onchange = save;
792
-
793
899
  const condRow = document.createElement('div');
794
900
  condRow.style.cssText = 'display:flex;gap:3px;';
795
901
  condRow.appendChild(condSel); condRow.appendChild(valInp);
796
902
  wrap.appendChild(condRow);
797
-
798
- wrap._getCtrl = () => ({
799
- oid: oidInp.value || undefined,
800
- condition: condSel.value,
801
- value: valInp.value
802
- });
903
+ wrap._getCtrl = () => ({ oid: oidInp.value || undefined, condition: condSel.value, value: valInp.value });
803
904
  return wrap;
804
905
  };
805
906
 
806
907
  const effectSel = sel([
807
- ['', '— none —'], ['opacity','Opacity'], ['rotation','Rotation'],
808
- ['scale','Scale'], ['translate','Translate XY'],
809
- ['translateX','Translate X'], ['translateY','Translate Y'],
810
- ['skew','Skew'], ['fill','Fill Color'], ['transform','Transform (CSS)'],
811
- ['svg','SVG Attribute'], ['morphSVG','MorphSVG'], ['motionPath','Motion Path']
908
+ ['','— none —'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
909
+ ['translate','Translate XY'],['translateX','Translate X'],['translateY','Translate Y'],
910
+ ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
911
+ ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
812
912
  ], cfg.effect || '');
813
913
 
814
914
  const svgAttrSel = sel([
815
- ['fill','fill'], ['color','color'], ['fill-opacity','fill-opacity'],
816
- ['stroke-opacity','stroke-opacity'], ['stroke-width','stroke-width'],
817
- ['stroke-dasharray','stroke-dasharray'], ['stroke-dashoffset','stroke-dashoffset'],
818
- ['x','x'], ['y','y'], ['cx','cx'], ['cy','cy'],
819
- ['r','r'], ['rx','rx'], ['ry','ry']
915
+ ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
916
+ ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
917
+ ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
918
+ ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
820
919
  ], cfg.svgAttr || 'fill');
821
920
 
822
921
  const easeSel = sel([
@@ -829,127 +928,82 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
829
928
  ], cfg.ease || 'power1.inOut');
830
929
 
831
930
  const valueFromInp = inp(cfg.valueFrom, 'text');
832
- const valueToInp = inp(cfg.valueTo, 'text');
833
- const durationInp = inp(cfg.duration ?? 1, 'number');
834
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
835
- const yoyoChk = chk(cfg.yoyo, 'yoyo');
836
- const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
837
- const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
838
- const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
839
- const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
840
- const pathIdInp = inp(cfg.pathId, 'text');
841
- const alignToPathChk = chk(cfg.alignToPath, 'align to path');
842
- const orientToPathChk = chk(cfg.orientToPath, 'auto-rotate');
843
-
844
- const playCtrl = oidRow('▶ Play', 'play', controls.play || {});
845
- const pauseCtrl = oidRow(' Pause', 'pause', controls.pause || {});
846
- const resumeCtrl = oidRow(' Resume', 'resume', controls.resume || {});
847
- const stopCtrl = oidRow(' Stop', 'stop', controls.stop || {});
848
- const reverseCtrl = oidRow(' Reverse', 'reverse', controls.reverse || {});
849
-
850
- const collectAnimCfg = () => {
851
- const e = effectSel.value;
852
- const c = {};
853
- const addCtrl = (key, row) => {
854
- const v = row._getCtrl();
855
- if (v.oid) c[key] = v;
856
- };
857
- addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
858
- addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
859
- addCtrl('reverse', reverseCtrl);
860
-
861
- const out = {};
862
- if (e) out.effect = e;
863
- if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
864
- if (valueFromInp.value) out.valueFrom = valueFromInp.value;
865
- if (valueToInp.value) out.valueTo = valueToInp.value;
866
- out.duration = parseFloat(durationInp.value) || 1;
867
- out.ease = easeSel.value || 'power1.inOut';
868
- out.repeat = parseInt(repeatInp.value) || 0;
869
- if (yoyoChk.querySelector('input').checked) out.yoyo = true;
870
- if (['rotation','scale'].includes(e)) {
871
- out.transformOriginX = originXInp.value;
872
- out.transformOriginY = originYInp.value;
873
- }
874
- if (e === 'fill' || e === 'svg') {
875
- if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
876
- if (fillToInp.value) out.fillColorTo = fillToInp.value;
877
- }
878
- if (e === 'motionPath') {
879
- if (pathIdInp.value) out.pathId = pathIdInp.value;
880
- if (alignToPathChk.querySelector('input').checked) out.alignToPath = true;
881
- if (orientToPathChk.querySelector('input').checked) out.orientToPath = true;
882
- }
883
- if (Object.keys(c).length) out.controls = c;
884
- return out;
885
- };
886
-
887
- // Build UI
931
+ const valueToInp = inp(cfg.valueTo, 'text');
932
+ const durationInp = inp(cfg.duration ?? 1, 'number');
933
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
934
+ const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
935
+ const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
936
+ const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
937
+ const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
938
+ const pathIdInp = inp(cfg.pathId, 'text');
939
+
940
+ const yoyoChk = chkField('yoyo', 'Yoyo', cfg.yoyo);
941
+ const alignToPathChk = chkField('alignToPath', 'Align to path', cfg.alignToPath);
942
+ const orientToPathChk= chkField('orientToPath', 'Auto-rotate', cfg.orientToPath);
943
+
944
+ const playCtrl = oidRow(' Play', controls.play || {});
945
+ const pauseCtrl = oidRow(' Pause', controls.pause || {});
946
+ const resumeCtrl = oidRow(' Resume', controls.resume || {});
947
+ const stopCtrl = oidRow(' Stop', controls.stop || {});
948
+ const reverseCtrl = oidRow('◀ Reverse', controls.reverse || {});
949
+
950
+ // ── Build UI ──────────────────────────────────────────────────────────
888
951
  content.innerHTML = '';
889
952
 
890
- // Clear button
891
953
  const clearBtn = document.createElement('button');
892
- clearBtn.textContent = 'Clear Animation';
954
+ clearBtn.textContent = 'Clear';
893
955
  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;';
894
956
  clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
895
957
  content.appendChild(clearBtn);
896
958
 
897
959
  const sec = (title) => {
898
960
  const d = document.createElement('div');
899
- d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:10px 0 5px;padding-top:8px;border-top:1px solid #eee;';
961
+ d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:10px 0 5px;padding-top:8px;border-top:1px solid #eee;clear:both;';
900
962
  d.textContent = title;
901
963
  return d;
902
964
  };
903
965
 
904
966
  content.appendChild(sec('Effect'));
905
- content.appendChild(field('Type', effectSel));
967
+ content.appendChild(field('Type', 'effect', effectSel));
906
968
 
907
- const svgAttrRow = field('SVG Attr', svgAttrSel);
969
+ const svgAttrRow = field('SVG Attr', 'svgAttr', svgAttrSel);
908
970
  svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
909
971
  content.appendChild(svgAttrRow);
910
- effectSel.addEventListener('change', () => {
911
- svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none';
912
- });
972
+ effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
913
973
 
914
- content.appendChild(field('Value From', valueFromInp));
915
- content.appendChild(field('Value To', valueToInp));
974
+ content.appendChild(field('Value From', 'valueFrom', valueFromInp));
975
+ content.appendChild(field('Value To', 'valueTo', valueToInp));
916
976
 
917
977
  const colorRows = document.createElement('div');
918
978
  colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
919
- colorRows.appendChild(field('Color From', fillFromInp));
920
- colorRows.appendChild(field('Color To', fillToInp));
979
+ colorRows.appendChild(field('Color From', 'fillColorFrom', fillFromInp));
980
+ colorRows.appendChild(field('Color To', 'fillColorTo', fillToInp));
921
981
  content.appendChild(colorRows);
922
- effectSel.addEventListener('change', () => {
923
- colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none';
924
- });
982
+ effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
925
983
 
926
984
  const motionRows = document.createElement('div');
927
985
  motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
928
- motionRows.appendChild(field('Path ID', pathIdInp));
986
+ motionRows.appendChild(field('Path ID', 'pathId', pathIdInp));
929
987
  motionRows.appendChild(alignToPathChk);
930
988
  motionRows.appendChild(orientToPathChk);
931
989
  content.appendChild(motionRows);
932
- effectSel.addEventListener('change', () => {
933
- motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none';
934
- });
990
+ effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
935
991
 
936
992
  content.appendChild(sec('Timing'));
937
- content.appendChild(field('Duration (s)', durationInp));
938
- content.appendChild(field('Ease', easeSel));
939
- content.appendChild(field('Repeat', repeatInp));
993
+ content.appendChild(field('Duration (s)', 'duration', durationInp));
994
+ content.appendChild(field('Ease', 'ease', easeSel));
995
+ content.appendChild(field('Repeat', 'repeat', repeatInp));
940
996
  content.appendChild(yoyoChk);
941
997
 
942
998
  const originRows = document.createElement('div');
943
999
  originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
944
1000
  originRows.appendChild(sec('Transform Origin'));
945
- originRows.appendChild(field('Origin X (%)', originXInp));
946
- originRows.appendChild(field('Origin Y (%)', originYInp));
1001
+ originRows.appendChild(field('Origin X (%)', 'transformOriginX', originXInp));
1002
+ originRows.appendChild(field('Origin Y (%)', 'transformOriginY', originYInp));
947
1003
  content.appendChild(originRows);
948
- effectSel.addEventListener('change', () => {
949
- originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none';
950
- });
1004
+ effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
951
1005
 
952
- content.appendChild(sec('Controls (OID bindings)'));
1006
+ content.appendChild(sec('Controls (OID triggers)'));
953
1007
  content.appendChild(playCtrl);
954
1008
  content.appendChild(pauseCtrl);
955
1009
  content.appendChild(resumeCtrl);
@@ -1002,18 +1056,52 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1002
1056
  let cfg = {};
1003
1057
  try { cfg = JSON.parse(element.getAttribute('data-effects') || '{}'); } catch (e) {}
1004
1058
 
1059
+ const collectEffectsCfg = () => {
1060
+ const out = {};
1061
+ if (typeSel.value) out.type = typeSel.value;
1062
+ out.trigger = triggerSel.value || 'load';
1063
+ out.duration = parseFloat(durationInp.value) || 0.5;
1064
+ if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1065
+ if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1066
+ out.ease = easeSel.value || 'power2.out';
1067
+ if (triggerSel.value === 'oid' && oidInp.value) {
1068
+ out.oid = oidInp.value;
1069
+ out.condition = condSel.value;
1070
+ out.conditionValue = condValInp.value;
1071
+ }
1072
+ if (typeSel.value === 'glow') {
1073
+ out.glowColor = glowColorInp.value;
1074
+ out.glowSize = parseInt(glowSizeInp.value) || 10;
1075
+ }
1076
+ if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1077
+ // Preserve all _bind properties
1078
+ for (const [k, v] of Object.entries(cfg)) {
1079
+ if (k.endsWith('_bind') && v) out[k] = v;
1080
+ }
1081
+ return out;
1082
+ };
1083
+
1005
1084
  const save = () => {
1006
1085
  const c = collectEffectsCfg();
1007
1086
  if (!c.type) designItem.removeAttribute('data-effects');
1008
1087
  else designItem.setAttribute('data-effects', JSON.stringify(c));
1009
1088
  };
1010
1089
 
1011
- const field = (label, inputEl) => {
1090
+ const saveAndRefresh = () => { save(); this._updateEffectsPanel(); };
1091
+
1092
+ const field = (label, propKey, inputEl) => {
1012
1093
  const row = document.createElement('div');
1013
- row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
1094
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
1095
+ if (propKey) {
1096
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-effects', saveAndRefresh));
1097
+ } else {
1098
+ const sp = document.createElement('div');
1099
+ sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
1100
+ row.appendChild(sp);
1101
+ }
1014
1102
  const lbl = document.createElement('span');
1015
1103
  lbl.textContent = label;
1016
- lbl.style.cssText = 'min-width:80px;font-size:11px;color:#555;';
1104
+ lbl.style.cssText = 'min-width:78px;font-size:11px;color:#555;';
1017
1105
  row.appendChild(lbl); row.appendChild(inputEl);
1018
1106
  return row;
1019
1107
  };
@@ -1049,107 +1137,75 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
1049
1137
  ], cfg.trigger || 'load');
1050
1138
 
1051
1139
  const durationInp = inp(cfg.duration ?? 0.5, 'number');
1052
- const delayInp = inp(cfg.delay ?? 0, 'number');
1053
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
1140
+ const delayInp = inp(cfg.delay ?? 0, 'number');
1141
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
1054
1142
 
1055
1143
  const easeSel = sel([
1056
1144
  ['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
1057
1145
  ['power1.inOut','power1.inOut'],['bounce.out','bounce.out'],
1058
- ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],
1059
- ['none','none']
1146
+ ['elastic.out(1,0.3)','elastic.out'],['back.inOut(1.7)','back.inOut'],['none','none']
1060
1147
  ], cfg.ease || 'power2.out');
1061
1148
 
1062
- const oidInp = inp(cfg.oid || '');
1063
- const condSel = sel([
1064
- ['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']
1065
- ], cfg.condition || 'equal');
1149
+ const oidInp = inp(cfg.oid || '');
1150
+ const condSel = sel([['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']], cfg.condition || 'equal');
1066
1151
  const condValInp = inp(cfg.conditionValue ?? 'true');
1067
-
1068
1152
  const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
1069
- const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1070
- const blurInp = inp(cfg.blurAmount || 5, 'number');
1153
+ const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1154
+ const blurInp = inp(cfg.blurAmount || 5, 'number');
1071
1155
 
1072
1156
  const oidPickBtn = document.createElement('button');
1073
1157
  oidPickBtn.textContent = '…';
1074
- oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;';
1158
+ oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;flex-shrink:0;';
1075
1159
  oidPickBtn.onclick = async () => {
1076
1160
  const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
1077
1161
  if (picked) { oidInp.value = picked; save(); }
1078
1162
  };
1079
1163
 
1080
- const collectEffectsCfg = () => {
1081
- const out = {};
1082
- if (typeSel.value) out.type = typeSel.value;
1083
- out.trigger = triggerSel.value || 'load';
1084
- out.duration = parseFloat(durationInp.value) || 0.5;
1085
- if (parseFloat(delayInp.value)) out.delay = parseFloat(delayInp.value);
1086
- if (parseInt(repeatInp.value)) out.repeat = parseInt(repeatInp.value);
1087
- out.ease = easeSel.value || 'power2.out';
1088
- if (triggerSel.value === 'oid' && oidInp.value) {
1089
- out.oid = oidInp.value;
1090
- out.condition = condSel.value;
1091
- out.conditionValue = condValInp.value;
1092
- }
1093
- if (typeSel.value === 'glow') {
1094
- out.glowColor = glowColorInp.value;
1095
- out.glowSize = parseInt(glowSizeInp.value) || 10;
1096
- }
1097
- if (typeSel.value === 'blur') out.blurAmount = parseInt(blurInp.value) || 5;
1098
- return out;
1099
- };
1100
-
1164
+ // ── Build UI ──────────────────────────────────────────────────────────
1101
1165
  content.innerHTML = '';
1102
1166
 
1103
1167
  const clearBtn = document.createElement('button');
1104
- clearBtn.textContent = 'Clear Effect';
1168
+ clearBtn.textContent = 'Clear';
1105
1169
  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;';
1106
1170
  clearBtn.onclick = () => { designItem.removeAttribute('data-effects'); this._updateEffectsPanel(); };
1107
1171
  content.appendChild(clearBtn);
1108
1172
 
1109
- content.appendChild(field('Type', typeSel));
1110
- content.appendChild(field('Trigger', triggerSel));
1111
- content.appendChild(field('Duration (s)', durationInp));
1112
- content.appendChild(field('Delay (s)', delayInp));
1113
- content.appendChild(field('Repeat', repeatInp));
1114
- content.appendChild(field('Ease', easeSel));
1173
+ content.appendChild(field('Type', 'type', typeSel));
1174
+ content.appendChild(field('Trigger', null, triggerSel));
1175
+ content.appendChild(field('Duration (s)', 'duration', durationInp));
1176
+ content.appendChild(field('Delay (s)', 'delay', delayInp));
1177
+ content.appendChild(field('Repeat', 'repeat', repeatInp));
1178
+ content.appendChild(field('Ease', 'ease', easeSel));
1115
1179
 
1116
1180
  const oidSection = document.createElement('div');
1117
1181
  oidSection.style.display = cfg.trigger === 'oid' ? '' : 'none';
1118
- const oidRow = document.createElement('div');
1119
- oidRow.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;';
1120
- oidRow.appendChild(oidInp); oidRow.appendChild(oidPickBtn);
1121
- oidSection.appendChild(field('OID', document.createElement('span')));
1122
- oidSection.lastChild.remove();
1123
1182
  const oidLabel = document.createElement('div');
1124
- oidLabel.style.cssText = 'font-size:11px;font-weight:600;color:#555;margin-bottom:3px;';
1183
+ oidLabel.style.cssText = 'font-size:11px;font-weight:600;color:#555;margin-bottom:3px;padding-left:15px;';
1125
1184
  oidLabel.textContent = 'OID';
1185
+ const oidRowDiv = document.createElement('div');
1186
+ oidRowDiv.style.cssText = 'display:flex;gap:3px;margin-bottom:4px;padding-left:15px;';
1187
+ oidRowDiv.appendChild(oidInp); oidRowDiv.appendChild(oidPickBtn);
1188
+ const condRowDiv = document.createElement('div');
1189
+ condRowDiv.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;padding-left:15px;';
1190
+ condRowDiv.appendChild(condSel); condRowDiv.appendChild(condValInp);
1126
1191
  oidSection.appendChild(oidLabel);
1127
- oidSection.appendChild(oidRow);
1128
- const condRow = document.createElement('div');
1129
- condRow.style.cssText = 'display:flex;gap:3px;margin-bottom:6px;';
1130
- condRow.appendChild(condSel); condRow.appendChild(condValInp);
1131
- oidSection.appendChild(condRow);
1192
+ oidSection.appendChild(oidRowDiv);
1193
+ oidSection.appendChild(condRowDiv);
1132
1194
  content.appendChild(oidSection);
1133
- triggerSel.addEventListener('change', () => {
1134
- oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none';
1135
- });
1195
+ triggerSel.addEventListener('change', () => { oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none'; });
1136
1196
 
1137
1197
  const glowSection = document.createElement('div');
1138
1198
  glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
1139
- glowSection.appendChild(field('Glow Color', glowColorInp));
1140
- glowSection.appendChild(field('Glow Size (px)', glowSizeInp));
1199
+ glowSection.appendChild(field('Glow Color', 'glowColor', glowColorInp));
1200
+ glowSection.appendChild(field('Glow Size', 'glowSize', glowSizeInp));
1141
1201
  content.appendChild(glowSection);
1142
- typeSel.addEventListener('change', () => {
1143
- glowSection.style.display = typeSel.value === 'glow' ? '' : 'none';
1144
- });
1202
+ typeSel.addEventListener('change', () => { glowSection.style.display = typeSel.value === 'glow' ? '' : 'none'; });
1145
1203
 
1146
1204
  const blurSection = document.createElement('div');
1147
1205
  blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
1148
- blurSection.appendChild(field('Blur (px)', blurInp));
1206
+ blurSection.appendChild(field('Blur (px)', 'blurAmount', blurInp));
1149
1207
  content.appendChild(blurSection);
1150
- typeSel.addEventListener('change', () => {
1151
- blurSection.style.display = typeSel.value === 'blur' ? '' : 'none';
1152
- });
1208
+ typeSel.addEventListener('change', () => { blurSection.style.display = typeSel.value === 'blur' ? '' : 'none'; });
1153
1209
  }
1154
1210
 
1155
1211
  /* Move to a Dock Spawn Helper */
@@ -176,6 +176,33 @@ class AnimationInstance {
176
176
  bindControl('resume', () => this.tween?.play());
177
177
  bindControl('stop', () => this.tween?.pause(0));
178
178
  bindControl('reverse', () => this.tween?.reverse());
179
+
180
+ // Dynamic property bindings — each prop can have a propKey_bind: {signal, expression}
181
+ const dynamicProps = [
182
+ 'effect', 'valueFrom', 'valueTo', 'duration', 'ease', 'repeat', 'yoyo',
183
+ 'fillColorFrom', 'fillColorTo', 'transformOriginX', 'transformOriginY',
184
+ 'svgAttr', 'pathId', 'alignToPath', 'orientToPath'
185
+ ];
186
+ for (const prop of dynamicProps) {
187
+ const bindCfg = this.cfg[prop + '_bind'];
188
+ if (!bindCfg?.signal) continue;
189
+ const propCapture = prop;
190
+ const handler = (id, state) => {
191
+ if (state?.val != null) {
192
+ this.cfg[propCapture] = state.val;
193
+ // Rebuild tween if currently playing
194
+ if (this.tween && !this.tween.paused()) this._play();
195
+ }
196
+ };
197
+ try {
198
+ iobrokerHandler.connection.subscribeState(bindCfg.signal, handler);
199
+ this._subs.push({ oid: bindCfg.signal, handler });
200
+ } catch (e) {}
201
+ try {
202
+ const state = await iobrokerHandler.connection.getState(bindCfg.signal);
203
+ if (state?.val != null) this.cfg[prop] = state.val;
204
+ } catch (e) {}
205
+ }
179
206
  }
180
207
 
181
208
  _play() {