iobroker.mywebui 1.37.59 → 1.37.62

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.59",
4
+ "version": "1.37.60",
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.59",
3
+ "version": "1.37.62",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -106,6 +106,12 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
106
106
  <div id="visibilityDock" title="Visibility" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
107
107
  </div>
108
108
 
109
+ <div id="effectsDock" title="Effects" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
110
+ </div>
111
+
112
+ <div id="animationsDock" title="Animations" style="overflow: auto; width: 100%;" dock-spawn-dock-to="attributeDock">
113
+ </div>
114
+
109
115
  <div id="translationDock" title="Translations" style="overflow: hidden; width: 100%; height: 100%;" dock-spawn-dock-to="attributeDock">
110
116
  <iobroker-webui-translation-editor id="translationEditor"></iobroker-webui-translation-editor>
111
117
  </div>
@@ -282,30 +288,26 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
282
288
  this._solutionExplorer.initialize(serviceContainer);
283
289
  this.propertyGrid.serviceContainer = serviceContainer;
284
290
 
285
- // Setup Visibility Panel after DOM is fully ready - multiple retries
286
- let retries = 0;
287
- const setupPanel = () => {
288
- try {
289
- const visibilityDock = this._getDomElement('visibilityDock');
290
- if (visibilityDock) {
291
- this._setupVisibilityPanel();
292
- console.log('✅ [Visibility] Panel setup completed after', retries, 'retries');
293
- } else if (retries < 15) {
294
- retries++;
295
- setTimeout(setupPanel, 300);
296
- } else {
297
- console.error('❌ [Visibility] Failed to find visibilityDock after 15 retries');
298
- }
299
- } catch (err) {
300
- if (retries < 15) {
301
- retries++;
302
- setTimeout(setupPanel, 300);
303
- } else {
304
- console.error('❌ [Visibility] Setup error:', err);
291
+ // Setup side panels after DOM is fully ready - with retries
292
+ const makeSetup = (dockId, setupFn, label) => {
293
+ let retries = 0;
294
+ const run = () => {
295
+ try {
296
+ const dock = this._getDomElement(dockId);
297
+ if (dock) { setupFn(); }
298
+ else if (retries < 15) { retries++; setTimeout(run, 300); }
299
+ else { console.error(`❌ [${label}] dock not found after 15 retries`); }
300
+ } catch (err) {
301
+ if (retries < 15) { retries++; setTimeout(run, 300); }
302
+ else { console.error(`❌ [${label}] setup error:`, err); }
305
303
  }
306
- }
304
+ };
305
+ return run;
307
306
  };
308
- setTimeout(setupPanel, 1000);
307
+
308
+ setTimeout(makeSetup('visibilityDock', () => this._setupVisibilityPanel(), 'Visibility'), 1000);
309
+ setTimeout(makeSetup('animationsDock', () => this._setupAnimationsPanel(), 'Animations'), 1200);
310
+ setTimeout(makeSetup('effectsDock', () => this._setupEffectsPanel(), 'Effects'), 1400);
309
311
  }
310
312
 
311
313
  _setupVisibilityPanel() {
@@ -648,6 +650,508 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
648
650
  actionDiv.appendChild(actionSelect);
649
651
  content.appendChild(actionDiv);
650
652
  }
653
+
654
+ // ─── Animations Panel ────────────────────────────────────────────────────
655
+
656
+ _setupAnimationsPanel() {
657
+ const dock = this._getDomElement('animationsDock');
658
+ if (!dock) return;
659
+ if (dock.querySelector('#anim-panel-container')) return;
660
+
661
+ const panel = document.createElement('div');
662
+ panel.id = 'anim-panel-container';
663
+ panel.style.cssText = 'padding:12px;';
664
+ panel.innerHTML = `
665
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:bold;color:#333;">🎬 Animation</h3>
666
+ <div id="anim-panel-content" style="font-size:12px;"><p style="color:#999;font-style:italic;">Select an element...</p></div>
667
+ `;
668
+ dock.appendChild(panel);
669
+
670
+ let lastEl = null;
671
+ const poll = () => {
672
+ try {
673
+ const items = this.propertyGrid?.propertyGrid?._selectedItems;
674
+ const el = items?.length === 1 ? items[0].element : null;
675
+ if (el !== lastEl) { lastEl = el; this._updateAnimationsPanel(); }
676
+ } catch (e) {}
677
+ };
678
+ setInterval(poll, 500);
679
+ }
680
+
681
+ _updateAnimationsPanel() {
682
+ const dock = this._getDomElement('animationsDock');
683
+ if (!dock) return;
684
+ const content = dock.querySelector('#anim-panel-content');
685
+ if (!content) return;
686
+
687
+ const items = this.propertyGrid?.propertyGrid?._selectedItems;
688
+ if (!items || items.length !== 1) {
689
+ content.innerHTML = '<p style="color:#999;font-style:italic;">Select an element to configure animations...</p>';
690
+ return;
691
+ }
692
+
693
+ const designItem = items[0];
694
+ const element = designItem.element;
695
+
696
+ let cfg = {};
697
+ try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
698
+ const controls = cfg.controls || {};
699
+
700
+ const save = () => {
701
+ const newCfg = collectAnimCfg();
702
+ if (Object.keys(newCfg).length === 0 || (!newCfg.effect && !newCfg.controls)) {
703
+ designItem.removeAttribute('data-animation');
704
+ } else {
705
+ designItem.setAttribute('data-animation', JSON.stringify(newCfg));
706
+ }
707
+ };
708
+
709
+ const field = (label, inputEl) => {
710
+ const row = document.createElement('div');
711
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
712
+ const lbl = document.createElement('span');
713
+ lbl.textContent = label;
714
+ lbl.style.cssText = 'min-width:90px;font-size:11px;color:#555;';
715
+ row.appendChild(lbl);
716
+ row.appendChild(inputEl);
717
+ return row;
718
+ };
719
+
720
+ const inp = (val, type = 'text', width = '100%') => {
721
+ 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;
727
+ };
728
+
729
+ const sel = (options, val) => {
730
+ const s = document.createElement('select');
731
+ s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
732
+ options.forEach(([v, t]) => {
733
+ const o = document.createElement('option');
734
+ o.value = v; o.textContent = t; o.selected = v === val;
735
+ s.appendChild(o);
736
+ });
737
+ s.onchange = save;
738
+ return s;
739
+ };
740
+
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;';
744
+ const c = document.createElement('input');
745
+ c.type = 'checkbox'; c.checked = val === true || val === 'true';
746
+ c.onchange = save;
747
+ wrap.appendChild(c);
748
+ wrap.appendChild(document.createTextNode(label));
749
+ return wrap;
750
+ };
751
+
752
+ const oidRow = (label, ctrlKey, ctrlCfg) => {
753
+ const wrap = document.createElement('div');
754
+ wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
755
+
756
+ const head = document.createElement('div');
757
+ head.style.cssText = 'font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
758
+ head.textContent = label;
759
+ wrap.appendChild(head);
760
+
761
+ const oidInp = document.createElement('input');
762
+ oidInp.type = 'text';
763
+ oidInp.value = ctrlCfg.oid || '';
764
+ oidInp.placeholder = 'OID…';
765
+ 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
+ oidInp.onchange = save;
767
+
768
+ const pickBtn = document.createElement('button');
769
+ pickBtn.textContent = '…';
770
+ pickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;margin-left:3px;';
771
+ pickBtn.onclick = async () => {
772
+ const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
773
+ if (picked) { oidInp.value = picked; save(); }
774
+ };
775
+
776
+ const oidRow2 = document.createElement('div');
777
+ oidRow2.style.cssText = 'display:flex;margin-bottom:3px;';
778
+ oidRow2.appendChild(oidInp);
779
+ oidRow2.appendChild(pickBtn);
780
+ wrap.appendChild(oidRow2);
781
+
782
+ const condSel = sel([
783
+ ['equal','='], ['not_equal','≠'], ['less_than','<'], ['less_equal','≤'],
784
+ ['greater_than','>'], ['greater_equal','≥'], ['exists','exists']
785
+ ], ctrlCfg.condition || 'equal');
786
+ condSel.style.cssText += 'width:70px;flex:none;margin-right:4px;';
787
+ const valInp = document.createElement('input');
788
+ valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
789
+ valInp.placeholder = 'value';
790
+ valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
791
+ condSel.onchange = save; valInp.onchange = save;
792
+
793
+ const condRow = document.createElement('div');
794
+ condRow.style.cssText = 'display:flex;gap:3px;';
795
+ condRow.appendChild(condSel); condRow.appendChild(valInp);
796
+ wrap.appendChild(condRow);
797
+
798
+ wrap._getCtrl = () => ({
799
+ oid: oidInp.value || undefined,
800
+ condition: condSel.value,
801
+ value: valInp.value
802
+ });
803
+ return wrap;
804
+ };
805
+
806
+ 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']
812
+ ], cfg.effect || '');
813
+
814
+ 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']
820
+ ], cfg.svgAttr || 'fill');
821
+
822
+ const easeSel = sel([
823
+ ['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
824
+ ['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
825
+ ['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
826
+ ['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
827
+ ['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
828
+ ['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
829
+ ], cfg.ease || 'power1.inOut');
830
+
831
+ 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
888
+ content.innerHTML = '';
889
+
890
+ // Clear button
891
+ const clearBtn = document.createElement('button');
892
+ clearBtn.textContent = 'Clear Animation';
893
+ 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
+ clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
895
+ content.appendChild(clearBtn);
896
+
897
+ const sec = (title) => {
898
+ 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;';
900
+ d.textContent = title;
901
+ return d;
902
+ };
903
+
904
+ content.appendChild(sec('Effect'));
905
+ content.appendChild(field('Type', effectSel));
906
+
907
+ const svgAttrRow = field('SVG Attr', svgAttrSel);
908
+ svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
909
+ content.appendChild(svgAttrRow);
910
+ effectSel.addEventListener('change', () => {
911
+ svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none';
912
+ });
913
+
914
+ content.appendChild(field('Value From', valueFromInp));
915
+ content.appendChild(field('Value To', valueToInp));
916
+
917
+ const colorRows = document.createElement('div');
918
+ colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
919
+ colorRows.appendChild(field('Color From', fillFromInp));
920
+ colorRows.appendChild(field('Color To', fillToInp));
921
+ content.appendChild(colorRows);
922
+ effectSel.addEventListener('change', () => {
923
+ colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none';
924
+ });
925
+
926
+ const motionRows = document.createElement('div');
927
+ motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
928
+ motionRows.appendChild(field('Path ID', pathIdInp));
929
+ motionRows.appendChild(alignToPathChk);
930
+ motionRows.appendChild(orientToPathChk);
931
+ content.appendChild(motionRows);
932
+ effectSel.addEventListener('change', () => {
933
+ motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none';
934
+ });
935
+
936
+ content.appendChild(sec('Timing'));
937
+ content.appendChild(field('Duration (s)', durationInp));
938
+ content.appendChild(field('Ease', easeSel));
939
+ content.appendChild(field('Repeat', repeatInp));
940
+ content.appendChild(yoyoChk);
941
+
942
+ const originRows = document.createElement('div');
943
+ originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
944
+ originRows.appendChild(sec('Transform Origin'));
945
+ originRows.appendChild(field('Origin X (%)', originXInp));
946
+ originRows.appendChild(field('Origin Y (%)', originYInp));
947
+ content.appendChild(originRows);
948
+ effectSel.addEventListener('change', () => {
949
+ originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none';
950
+ });
951
+
952
+ content.appendChild(sec('Controls (OID bindings)'));
953
+ content.appendChild(playCtrl);
954
+ content.appendChild(pauseCtrl);
955
+ content.appendChild(resumeCtrl);
956
+ content.appendChild(stopCtrl);
957
+ content.appendChild(reverseCtrl);
958
+ }
959
+
960
+ // ─── Effects Panel ───────────────────────────────────────────────────────
961
+
962
+ _setupEffectsPanel() {
963
+ const dock = this._getDomElement('effectsDock');
964
+ if (!dock) return;
965
+ if (dock.querySelector('#effects-panel-container')) return;
966
+
967
+ const panel = document.createElement('div');
968
+ panel.id = 'effects-panel-container';
969
+ panel.style.cssText = 'padding:12px;';
970
+ panel.innerHTML = `
971
+ <h3 style="margin:0 0 12px 0;font-size:14px;font-weight:bold;color:#333;">✨ Effect</h3>
972
+ <div id="effects-panel-content" style="font-size:12px;"><p style="color:#999;font-style:italic;">Select an element...</p></div>
973
+ `;
974
+ dock.appendChild(panel);
975
+
976
+ let lastEl = null;
977
+ const poll = () => {
978
+ try {
979
+ const items = this.propertyGrid?.propertyGrid?._selectedItems;
980
+ const el = items?.length === 1 ? items[0].element : null;
981
+ if (el !== lastEl) { lastEl = el; this._updateEffectsPanel(); }
982
+ } catch (e) {}
983
+ };
984
+ setInterval(poll, 500);
985
+ }
986
+
987
+ _updateEffectsPanel() {
988
+ const dock = this._getDomElement('effectsDock');
989
+ if (!dock) return;
990
+ const content = dock.querySelector('#effects-panel-content');
991
+ if (!content) return;
992
+
993
+ const items = this.propertyGrid?.propertyGrid?._selectedItems;
994
+ if (!items || items.length !== 1) {
995
+ content.innerHTML = '<p style="color:#999;font-style:italic;">Select an element to configure effects...</p>';
996
+ return;
997
+ }
998
+
999
+ const designItem = items[0];
1000
+ const element = designItem.element;
1001
+
1002
+ let cfg = {};
1003
+ try { cfg = JSON.parse(element.getAttribute('data-effects') || '{}'); } catch (e) {}
1004
+
1005
+ const save = () => {
1006
+ const c = collectEffectsCfg();
1007
+ if (!c.type) designItem.removeAttribute('data-effects');
1008
+ else designItem.setAttribute('data-effects', JSON.stringify(c));
1009
+ };
1010
+
1011
+ const field = (label, inputEl) => {
1012
+ const row = document.createElement('div');
1013
+ row.style.cssText = 'display:flex;align-items:center;gap:6px;margin-bottom:6px;';
1014
+ const lbl = document.createElement('span');
1015
+ lbl.textContent = label;
1016
+ lbl.style.cssText = 'min-width:80px;font-size:11px;color:#555;';
1017
+ row.appendChild(lbl); row.appendChild(inputEl);
1018
+ return row;
1019
+ };
1020
+
1021
+ const inp = (val, type = 'text') => {
1022
+ const i = document.createElement('input');
1023
+ i.type = type; i.value = val ?? '';
1024
+ i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1025
+ i.onchange = save; return i;
1026
+ };
1027
+
1028
+ const sel = (options, val) => {
1029
+ const s = document.createElement('select');
1030
+ s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
1031
+ options.forEach(([v, t]) => {
1032
+ const o = document.createElement('option');
1033
+ o.value = v; o.textContent = t; o.selected = v === val;
1034
+ s.appendChild(o);
1035
+ });
1036
+ s.onchange = save; return s;
1037
+ };
1038
+
1039
+ const typeSel = sel([
1040
+ ['','— none —'],['fadeIn','Fade In'],['fadeOut','Fade Out'],
1041
+ ['slideInLeft','Slide In Left'],['slideInRight','Slide In Right'],
1042
+ ['slideInTop','Slide In Top'],['slideInBottom','Slide In Bottom'],
1043
+ ['bounce','Bounce'],['pulse','Pulse'],['shake','Shake'],
1044
+ ['glow','Glow'],['blur','Blur'],['spin','Spin'],['flip','Flip 3D']
1045
+ ], cfg.type || '');
1046
+
1047
+ const triggerSel = sel([
1048
+ ['load','On Load'],['hover','On Hover'],['click','On Click'],['oid','OID State']
1049
+ ], cfg.trigger || 'load');
1050
+
1051
+ const durationInp = inp(cfg.duration ?? 0.5, 'number');
1052
+ const delayInp = inp(cfg.delay ?? 0, 'number');
1053
+ const repeatInp = inp(cfg.repeat ?? 0, 'number');
1054
+
1055
+ const easeSel = sel([
1056
+ ['power2.out','power2.out'],['power2.in','power2.in'],['power2.inOut','power2.inOut'],
1057
+ ['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']
1060
+ ], cfg.ease || 'power2.out');
1061
+
1062
+ const oidInp = inp(cfg.oid || '');
1063
+ const condSel = sel([
1064
+ ['equal','='],['not_equal','≠'],['less_than','<'],['greater_than','>'],['exists','exists']
1065
+ ], cfg.condition || 'equal');
1066
+ const condValInp = inp(cfg.conditionValue ?? 'true');
1067
+
1068
+ const glowColorInp = inp(cfg.glowColor || 'yellow', 'color');
1069
+ const glowSizeInp = inp(cfg.glowSize || 10, 'number');
1070
+ const blurInp = inp(cfg.blurAmount || 5, 'number');
1071
+
1072
+ const oidPickBtn = document.createElement('button');
1073
+ oidPickBtn.textContent = '…';
1074
+ oidPickBtn.style.cssText = 'padding:2px 6px;font-size:11px;cursor:pointer;border:1px solid #aaa;border-radius:3px;';
1075
+ oidPickBtn.onclick = async () => {
1076
+ const picked = await openSelectIdDialog(document.body, { id: oidInp.value });
1077
+ if (picked) { oidInp.value = picked; save(); }
1078
+ };
1079
+
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
+
1101
+ content.innerHTML = '';
1102
+
1103
+ const clearBtn = document.createElement('button');
1104
+ clearBtn.textContent = 'Clear Effect';
1105
+ 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
+ clearBtn.onclick = () => { designItem.removeAttribute('data-effects'); this._updateEffectsPanel(); };
1107
+ content.appendChild(clearBtn);
1108
+
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));
1115
+
1116
+ const oidSection = document.createElement('div');
1117
+ 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
+ const oidLabel = document.createElement('div');
1124
+ oidLabel.style.cssText = 'font-size:11px;font-weight:600;color:#555;margin-bottom:3px;';
1125
+ oidLabel.textContent = 'OID';
1126
+ 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);
1132
+ content.appendChild(oidSection);
1133
+ triggerSel.addEventListener('change', () => {
1134
+ oidSection.style.display = triggerSel.value === 'oid' ? '' : 'none';
1135
+ });
1136
+
1137
+ const glowSection = document.createElement('div');
1138
+ glowSection.style.display = cfg.type === 'glow' ? '' : 'none';
1139
+ glowSection.appendChild(field('Glow Color', glowColorInp));
1140
+ glowSection.appendChild(field('Glow Size (px)', glowSizeInp));
1141
+ content.appendChild(glowSection);
1142
+ typeSel.addEventListener('change', () => {
1143
+ glowSection.style.display = typeSel.value === 'glow' ? '' : 'none';
1144
+ });
1145
+
1146
+ const blurSection = document.createElement('div');
1147
+ blurSection.style.display = cfg.type === 'blur' ? '' : 'none';
1148
+ blurSection.appendChild(field('Blur (px)', blurInp));
1149
+ content.appendChild(blurSection);
1150
+ typeSel.addEventListener('change', () => {
1151
+ blurSection.style.display = typeSel.value === 'blur' ? '' : 'none';
1152
+ });
1153
+ }
1154
+
651
1155
  /* Move to a Dock Spawn Helper */
652
1156
  activateDockById(name) {
653
1157
  this.activateDock(this._getDomElement(name));