iobroker.mywebui 1.37.68 → 1.37.70

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.68",
4
+ "version": "1.37.70",
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.68",
3
+ "version": "1.37.70",
4
4
  "description": "ioBroker mywebui - Custom edited mywebui by gokturk413",
5
5
  "type": "module",
6
6
  "main": "dist/backend/main.js",
@@ -760,85 +760,32 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
760
760
  const designItem = items[0];
761
761
  const element = designItem.element;
762
762
 
763
- let cfg = {};
764
- try { cfg = JSON.parse(element.getAttribute('data-animation') || '{}'); } catch (e) {}
765
- const controls = cfg.controls || {};
766
-
767
- const collectAnimCfg = () => {
768
- const e = effectSel.value;
769
- const c = {};
770
- const addCtrl = (key, row) => { const v = row._getCtrl(); if (v.oid || v.oid_bind) 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
-
804
- const save = () => {
805
- const newCfg = collectAnimCfg();
806
- if (!newCfg.effect && !newCfg.controls && !Object.keys(newCfg).some(k => k.endsWith('_bind'))) {
807
- designItem.removeAttribute('data-animation');
808
- } else {
809
- designItem.setAttribute('data-animation', JSON.stringify(newCfg));
810
- }
763
+ // Support both single object (legacy) and array
764
+ let cfgList = [];
765
+ try {
766
+ const raw = JSON.parse(element.getAttribute('data-animation') || '[]');
767
+ cfgList = Array.isArray(raw) ? raw : [raw];
768
+ } catch (e) {}
769
+
770
+ const saveAll = () => {
771
+ const result = cfgList.filter(c => c._collect && (c._collect().effect || Object.keys(c._collect()).some(k => k.endsWith('_bind'))));
772
+ const data = result.map(c => c._collect());
773
+ if (data.length === 0) designItem.removeAttribute('data-animation');
774
+ else if (data.length === 1) designItem.setAttribute('data-animation', JSON.stringify(data[0]));
775
+ else designItem.setAttribute('data-animation', JSON.stringify(data));
811
776
  };
777
+ const saveAndRefresh = () => { saveAll(); this._updateAnimationsPanel(); };
812
778
 
813
- const saveAndRefresh = () => { save(); this._updateAnimationsPanel(); };
779
+ // ── Shared helpers ────────────────────────────────────────────────────
814
780
 
815
- // field(label, propKey, inputEl) propKey=null → no binding square
816
- const field = (label, propKey, inputEl) => {
817
- const row = document.createElement('div');
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
- }
826
- const lbl = document.createElement('span');
827
- lbl.textContent = label;
828
- lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
829
- row.appendChild(lbl);
830
- row.appendChild(inputEl);
831
- return row;
832
- };
833
-
834
- const inp = (val, type = 'text') => {
781
+ const mkInp = (val, type, onchange) => {
835
782
  const i = document.createElement('input');
836
783
  i.type = type; i.value = val ?? '';
837
784
  i.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
838
- i.onchange = save; return i;
785
+ i.onchange = onchange; return i;
839
786
  };
840
787
 
841
- const sel = (options, val) => {
788
+ const mkSel = (options, val, onchange) => {
842
789
  const s = document.createElement('select');
843
790
  s.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
844
791
  options.forEach(([v, t]) => {
@@ -846,54 +793,60 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
846
793
  o.value = v; o.textContent = t; o.selected = v === val;
847
794
  s.appendChild(o);
848
795
  });
849
- s.onchange = save; return s;
796
+ s.onchange = onchange; return s;
850
797
  };
851
798
 
852
- const chkField = (propKey, label, val) => {
799
+ const mkField = (label, propKey, inputEl, cfg) => {
800
+ const row = document.createElement('div');
801
+ row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
802
+ if (propKey) {
803
+ row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
804
+ } else {
805
+ const sp = document.createElement('div'); sp.style.cssText = 'width:11px;min-width:11px;flex-shrink:0;';
806
+ row.appendChild(sp);
807
+ }
808
+ const lbl = document.createElement('span');
809
+ lbl.textContent = label; lbl.style.cssText = 'min-width:84px;font-size:11px;color:#555;';
810
+ row.appendChild(lbl); row.appendChild(inputEl);
811
+ return row;
812
+ };
813
+
814
+ const mkChkField = (propKey, label, val, cfg, onchange) => {
853
815
  const row = document.createElement('div');
854
816
  row.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:6px;';
855
817
  row.appendChild(this._makeBindSquare(propKey, cfg, designItem, 'data-animation', saveAndRefresh));
856
818
  const lbl = document.createElement('label');
857
819
  lbl.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;cursor:pointer;';
858
820
  const c = document.createElement('input');
859
- c.type = 'checkbox'; c.checked = val === true || val === 'true';
860
- c.onchange = save;
861
- lbl.appendChild(c);
862
- lbl.appendChild(document.createTextNode(label));
821
+ c.type = 'checkbox'; c.checked = val === true || val === 'true'; c.onchange = onchange;
822
+ lbl.appendChild(c); lbl.appendChild(document.createTextNode(label));
863
823
  row.appendChild(lbl);
824
+ row._checked = () => c.checked;
864
825
  return row;
865
826
  };
866
827
 
867
- const oidRow = (label, ctrlCfg) => {
828
+ const mkOidRow = (label, ctrlCfg, onchange) => {
868
829
  const wrap = document.createElement('div');
869
830
  wrap.style.cssText = 'border:1px solid #eee;border-radius:3px;padding:6px;margin-bottom:6px;background:#fafafa;';
870
831
  const head = document.createElement('div');
871
832
  head.style.cssText = 'display:flex;align-items:center;gap:4px;font-size:11px;font-weight:600;color:#444;margin-bottom:5px;';
872
833
  head.appendChild(this._makeBindSquare('oid', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
873
- const headText = document.createElement('span');
874
- headText.textContent = label;
875
- head.appendChild(headText);
876
- wrap.appendChild(head);
877
- const condSel = sel([
834
+ const headText = document.createElement('span'); headText.textContent = label;
835
+ head.appendChild(headText); wrap.appendChild(head);
836
+ const condSel = mkSel([
878
837
  ['equal','='],['not_equal','≠'],['less_than','<'],['less_equal','≤'],
879
838
  ['greater_than','>'],['greater_equal','≥'],['exists','exists']
880
- ], ctrlCfg.condition || 'equal');
839
+ ], ctrlCfg.condition || 'equal', onchange);
881
840
  condSel.style.cssText += 'flex:1;';
882
- condSel.onchange = save;
883
841
  const condRow = document.createElement('div');
884
842
  condRow.style.cssText = 'display:flex;align-items:center;gap:4px;margin-bottom:4px;';
885
843
  condRow.appendChild(this._makeBindSquare('condition', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
886
- condRow.appendChild(condSel);
887
- wrap.appendChild(condRow);
888
- const valInp = document.createElement('input');
889
- valInp.type = 'text'; valInp.value = ctrlCfg.value ?? 'true';
890
- valInp.style.cssText = 'flex:1;padding:3px 5px;font-size:11px;border:1px solid #ccc;border-radius:3px;';
891
- valInp.onchange = save;
844
+ condRow.appendChild(condSel); wrap.appendChild(condRow);
845
+ const valInp = mkInp(ctrlCfg.value ?? 'true', 'text', onchange);
892
846
  const valRow = document.createElement('div');
893
847
  valRow.style.cssText = 'display:flex;align-items:center;gap:4px;';
894
848
  valRow.appendChild(this._makeBindSquare('value', ctrlCfg, designItem, 'data-animation', saveAndRefresh));
895
- valRow.appendChild(valInp);
896
- wrap.appendChild(valRow);
849
+ valRow.appendChild(valInp); wrap.appendChild(valRow);
897
850
  wrap._getCtrl = () => {
898
851
  const v = { condition: condSel.value, value: valInp.value };
899
852
  if (ctrlCfg.oid_bind) v.oid_bind = ctrlCfg.oid_bind;
@@ -904,112 +857,198 @@ export class IobrokerWebuiAppShell extends BaseCustomWebComponentConstructorAppe
904
857
  return wrap;
905
858
  };
906
859
 
907
- const effectSel = sel([
908
- ['','— none —'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
909
- ['translateX','Move X (relative)'],['translateY','Move Y (relative)'],['translate','Move XY (relative)'],
910
- ['left','Move Left (absolute)'],['top','Move Top (absolute)'],
911
- ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
912
- ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
913
- ], cfg.effect || '');
914
-
915
- const svgAttrSel = sel([
916
- ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
917
- ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
918
- ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
919
- ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
920
- ], cfg.svgAttr || 'fill');
860
+ // ── Build one animation block ─────────────────────────────────────────
861
+
862
+ const buildAnimBlock = (cfg, index) => {
863
+ const save = saveAll;
864
+ const controls = cfg.controls || {};
865
+
866
+ const block = document.createElement('div');
867
+ block.style.cssText = 'border:2px solid #c8d6e0;border-radius:5px;padding:8px;margin-bottom:10px;background:#f7fafc;';
868
+
869
+ // Header: "Animation #N" + delete button
870
+ const hdr = document.createElement('div');
871
+ hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;';
872
+ const hdrTitle = document.createElement('span');
873
+ hdrTitle.textContent = `Animation #${index + 1}`;
874
+ hdrTitle.style.cssText = 'font-size:12px;font-weight:700;color:#3a6a8a;';
875
+ const delBtn = document.createElement('button');
876
+ delBtn.textContent = '✕ Remove';
877
+ delBtn.style.cssText = 'font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #c66;border-radius:3px;background:#fff;color:#c33;';
878
+ delBtn.onclick = () => { cfgList.splice(index, 1); saveAndRefresh(); };
879
+ hdr.appendChild(hdrTitle); hdr.appendChild(delBtn);
880
+ block.appendChild(hdr);
881
+
882
+ const sec = (title) => {
883
+ const d = document.createElement('div');
884
+ d.style.cssText = 'font-size:11px;font-weight:700;color:#555;margin:8px 0 4px;padding-top:6px;border-top:1px solid #dde;clear:both;';
885
+ d.textContent = title; return d;
886
+ };
921
887
 
922
- const easeSel = sel([
923
- ['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
924
- ['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
925
- ['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
926
- ['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
927
- ['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
928
- ['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
929
- ], cfg.ease || 'power1.inOut');
930
-
931
- const valueFromInp = inp(cfg.valueFrom, 'text');
932
- const valueToInp = inp(cfg.valueTo, 'text');
933
- const durationInp = inp(cfg.duration ?? 1, 'number');
934
- const repeatInp = inp(cfg.repeat ?? 0, 'number');
935
- const originXInp = inp(cfg.transformOriginX ?? '50', 'number');
936
- const originYInp = inp(cfg.transformOriginY ?? '50', 'number');
937
- const fillFromInp = inp(cfg.fillColorFrom || '#000000', 'color');
938
- const fillToInp = inp(cfg.fillColorTo || '#ff0000', 'color');
939
- const pathIdInp = inp(cfg.pathId, 'text');
940
-
941
- const yoyoChk = chkField('yoyo', 'Yoyo', cfg.yoyo);
942
- const alignToPathChk = chkField('alignToPath', 'Align to path', cfg.alignToPath);
943
- const orientToPathChk= chkField('orientToPath', 'Auto-rotate', cfg.orientToPath);
944
-
945
- const playCtrl = oidRow('▶ Play', controls.play || {});
946
- const pauseCtrl = oidRow('⏸ Pause', controls.pause || {});
947
- const resumeCtrl = oidRow('▶ Resume', controls.resume || {});
948
- const stopCtrl = oidRow('⏹ Stop', controls.stop || {});
949
- const reverseCtrl = oidRow('◀ Reverse', controls.reverse || {});
888
+ const effectSel = mkSel([
889
+ ['','none'],['opacity','Opacity'],['rotation','Rotation'],['scale','Scale'],
890
+ ['translateX','Move X (relative)'],['translateY','Move Y (relative)'],['translate','Move XY (relative)'],
891
+ ['left','Move Left (absolute)'],['top','Move Top (absolute)'],
892
+ ['skew','Skew'],['fill','Fill Color'],['transform','Transform (CSS)'],
893
+ ['svg','SVG Attribute'],['morphSVG','MorphSVG'],['motionPath','Motion Path']
894
+ ], cfg.effect || '', save);
895
+
896
+ const svgAttrSel = mkSel([
897
+ ['fill','fill'],['color','color'],['fill-opacity','fill-opacity'],
898
+ ['stroke-opacity','stroke-opacity'],['stroke-width','stroke-width'],
899
+ ['stroke-dasharray','stroke-dasharray'],['stroke-dashoffset','stroke-dashoffset'],
900
+ ['x','x'],['y','y'],['cx','cx'],['cy','cy'],['r','r'],['rx','rx'],['ry','ry']
901
+ ], cfg.svgAttr || 'fill', save);
902
+
903
+ const easeSel = mkSel([
904
+ ['none','none'],['power1.in','power1.in'],['power1.out','power1.out'],
905
+ ['power1.inOut','power1.inOut'],['power2.in','power2.in'],['power2.out','power2.out'],
906
+ ['power2.inOut','power2.inOut'],['power3.inOut','power3.inOut'],
907
+ ['bounce.out','bounce.out'],['elastic.out(1,0.3)','elastic.out'],
908
+ ['back.inOut(1.7)','back.inOut'],['circ.inOut','circ.inOut'],
909
+ ['expo.inOut','expo.inOut'],['sine.inOut','sine.inOut']
910
+ ], cfg.ease || 'power1.inOut', save);
911
+
912
+ const valueFromInp = mkInp(cfg.valueFrom, 'text', save);
913
+ const valueToInp = mkInp(cfg.valueTo, 'text', save);
914
+ const durationInp = mkInp(cfg.duration ?? 1, 'number', save);
915
+ const repeatInp = mkInp(cfg.repeat ?? 0, 'number', save);
916
+ const originXInp = mkInp(cfg.transformOriginX ?? '50', 'number', save);
917
+ const originYInp = mkInp(cfg.transformOriginY ?? '50', 'number', save);
918
+ const fillFromInp = mkInp(cfg.fillColorFrom || '#000000', 'color', save);
919
+ const fillToInp = mkInp(cfg.fillColorTo || '#ff0000', 'color', save);
920
+ const pathIdInp = mkInp(cfg.pathId, 'text', save);
921
+
922
+ const yoyoChk = mkChkField('yoyo', 'Yoyo', cfg.yoyo, cfg, save);
923
+ const alignToPathChk = mkChkField('alignToPath', 'Align to path', cfg.alignToPath, cfg, save);
924
+ const orientToPathChk= mkChkField('orientToPath', 'Auto-rotate', cfg.orientToPath, cfg, save);
925
+
926
+ const playCtrl = mkOidRow('▶ Play', controls.play || {}, save);
927
+ const pauseCtrl = mkOidRow('⏸ Pause', controls.pause || {}, save);
928
+ const resumeCtrl = mkOidRow('▶ Resume', controls.resume || {}, save);
929
+ const stopCtrl = mkOidRow('⏹ Stop', controls.stop || {}, save);
930
+ const reverseCtrl = mkOidRow('◀ Reverse', controls.reverse || {}, save);
931
+
932
+ // _collect: reads all UI values back into a config object
933
+ cfg._collect = () => {
934
+ const e = effectSel.value;
935
+ const c = {};
936
+ const addCtrl = (key, row) => { const v = row._getCtrl(); if (v.oid || v.oid_bind) c[key] = v; };
937
+ addCtrl('play', playCtrl); addCtrl('pause', pauseCtrl);
938
+ addCtrl('resume', resumeCtrl); addCtrl('stop', stopCtrl);
939
+ addCtrl('reverse', reverseCtrl);
940
+ const out = {};
941
+ if (e) out.effect = e;
942
+ if (e === 'svg' && svgAttrSel.value) out.svgAttr = svgAttrSel.value;
943
+ if (valueFromInp.value) out.valueFrom = valueFromInp.value;
944
+ if (valueToInp.value) out.valueTo = valueToInp.value;
945
+ out.duration = parseFloat(durationInp.value) || 1;
946
+ out.ease = easeSel.value || 'power1.inOut';
947
+ out.repeat = parseInt(repeatInp.value) || 0;
948
+ if (yoyoChk._checked()) out.yoyo = true;
949
+ if (['rotation','scale'].includes(e)) {
950
+ out.transformOriginX = originXInp.value;
951
+ out.transformOriginY = originYInp.value;
952
+ }
953
+ if (e === 'fill' || e === 'svg') {
954
+ if (fillFromInp.value) out.fillColorFrom = fillFromInp.value;
955
+ if (fillToInp.value) out.fillColorTo = fillToInp.value;
956
+ }
957
+ if (e === 'motionPath') {
958
+ if (pathIdInp.value) out.pathId = pathIdInp.value;
959
+ if (alignToPathChk._checked()) out.alignToPath = true;
960
+ if (orientToPathChk._checked()) out.orientToPath = true;
961
+ }
962
+ if (Object.keys(c).length) out.controls = c;
963
+ for (const [k, v] of Object.entries(cfg)) {
964
+ if (k.endsWith('_bind') && v) out[k] = v;
965
+ }
966
+ return out;
967
+ };
950
968
 
951
- // ── Build UI ──────────────────────────────────────────────────────────
969
+ // Build block DOM
970
+ block.appendChild(sec('Effect'));
971
+ block.appendChild(mkField('Type', 'effect', effectSel, cfg));
972
+
973
+ const svgAttrRow = mkField('SVG Attr', 'svgAttr', svgAttrSel, cfg);
974
+ svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
975
+ block.appendChild(svgAttrRow);
976
+ effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
977
+
978
+ block.appendChild(mkField('Value From', 'valueFrom', valueFromInp, cfg));
979
+ block.appendChild(mkField('Value To', 'valueTo', valueToInp, cfg));
980
+
981
+ const colorRows = document.createElement('div');
982
+ colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
983
+ colorRows.appendChild(mkField('Color From', 'fillColorFrom', fillFromInp, cfg));
984
+ colorRows.appendChild(mkField('Color To', 'fillColorTo', fillToInp, cfg));
985
+ block.appendChild(colorRows);
986
+ effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
987
+
988
+ const motionRows = document.createElement('div');
989
+ motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
990
+ motionRows.appendChild(mkField('Path ID', 'pathId', pathIdInp, cfg));
991
+ motionRows.appendChild(alignToPathChk);
992
+ motionRows.appendChild(orientToPathChk);
993
+ block.appendChild(motionRows);
994
+ effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
995
+
996
+ block.appendChild(sec('Timing'));
997
+ block.appendChild(mkField('Duration (s)', 'duration', durationInp, cfg));
998
+ block.appendChild(mkField('Ease', 'ease', easeSel, cfg));
999
+ block.appendChild(mkField('Repeat', 'repeat', repeatInp, cfg));
1000
+ block.appendChild(yoyoChk);
1001
+
1002
+ const originRows = document.createElement('div');
1003
+ originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
1004
+ originRows.appendChild(sec('Transform Origin'));
1005
+ originRows.appendChild(mkField('Origin X (%)', 'transformOriginX', originXInp, cfg));
1006
+ originRows.appendChild(mkField('Origin Y (%)', 'transformOriginY', originYInp, cfg));
1007
+ block.appendChild(originRows);
1008
+ effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
1009
+
1010
+ block.appendChild(sec('Controls (OID triggers)'));
1011
+ block.appendChild(playCtrl); block.appendChild(pauseCtrl);
1012
+ block.appendChild(resumeCtrl); block.appendChild(stopCtrl);
1013
+ block.appendChild(reverseCtrl);
1014
+
1015
+ return block;
1016
+ };
1017
+
1018
+ // ── Render ────────────────────────────────────────────────────────────
952
1019
  content.innerHTML = '';
953
1020
 
954
- const clearBtn = document.createElement('button');
955
- clearBtn.textContent = 'Clear';
956
- 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;';
957
- clearBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
958
- content.appendChild(clearBtn);
1021
+ const clearAllBtn = document.createElement('button');
1022
+ clearAllBtn.textContent = 'Clear All';
1023
+ clearAllBtn.style.cssText = 'float:right;font-size:10px;padding:2px 6px;cursor:pointer;border:1px solid #ccc;border-radius:3px;background:#fff;margin-bottom:8px;';
1024
+ clearAllBtn.onclick = () => { designItem.removeAttribute('data-animation'); this._updateAnimationsPanel(); };
1025
+ content.appendChild(clearAllBtn);
1026
+
1027
+ if (cfgList.length === 0) {
1028
+ const hint = document.createElement('p');
1029
+ hint.style.cssText = 'color:#999;font-style:italic;font-size:11px;';
1030
+ hint.textContent = 'No animations. Click "+ Add Animation" to start.';
1031
+ content.appendChild(hint);
1032
+ }
959
1033
 
960
- const sec = (title) => {
961
- const d = document.createElement('div');
962
- 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;';
963
- d.textContent = title;
964
- return d;
1034
+ cfgList.forEach((cfg, i) => content.appendChild(buildAnimBlock(cfg, i)));
1035
+
1036
+ const addBtn = document.createElement('button');
1037
+ addBtn.textContent = '+ Add Animation';
1038
+ addBtn.style.cssText = 'width:100%;margin-top:6px;padding:5px;font-size:12px;cursor:pointer;border:1px solid #5a9;border-radius:4px;background:#e8f8f0;color:#2a7a5a;font-weight:600;';
1039
+ addBtn.onclick = () => {
1040
+ // Collect currently valid animations from UI
1041
+ const existing = cfgList
1042
+ .filter(c => c._collect)
1043
+ .map(c => c._collect())
1044
+ .filter(c => c.effect || Object.keys(c).some(k => k.endsWith('_bind')));
1045
+ // Add new empty animation with a default effect so it survives save/reload
1046
+ existing.push({ effect: 'opacity', duration: 1, ease: 'power1.inOut', repeat: 0 });
1047
+ const val = existing.length === 1 ? existing[0] : existing;
1048
+ designItem.setAttribute('data-animation', JSON.stringify(val));
1049
+ this._updateAnimationsPanel();
965
1050
  };
966
-
967
- content.appendChild(sec('Effect'));
968
- content.appendChild(field('Type', 'effect', effectSel));
969
-
970
- const svgAttrRow = field('SVG Attr', 'svgAttr', svgAttrSel);
971
- svgAttrRow.style.display = cfg.effect === 'svg' ? '' : 'none';
972
- content.appendChild(svgAttrRow);
973
- effectSel.addEventListener('change', () => { svgAttrRow.style.display = effectSel.value === 'svg' ? '' : 'none'; });
974
-
975
- content.appendChild(field('Value From', 'valueFrom', valueFromInp));
976
- content.appendChild(field('Value To', 'valueTo', valueToInp));
977
-
978
- const colorRows = document.createElement('div');
979
- colorRows.style.display = (cfg.effect === 'fill' || cfg.effect === 'svg') ? '' : 'none';
980
- colorRows.appendChild(field('Color From', 'fillColorFrom', fillFromInp));
981
- colorRows.appendChild(field('Color To', 'fillColorTo', fillToInp));
982
- content.appendChild(colorRows);
983
- effectSel.addEventListener('change', () => { colorRows.style.display = (effectSel.value === 'fill' || effectSel.value === 'svg') ? '' : 'none'; });
984
-
985
- const motionRows = document.createElement('div');
986
- motionRows.style.display = cfg.effect === 'motionPath' ? '' : 'none';
987
- motionRows.appendChild(field('Path ID', 'pathId', pathIdInp));
988
- motionRows.appendChild(alignToPathChk);
989
- motionRows.appendChild(orientToPathChk);
990
- content.appendChild(motionRows);
991
- effectSel.addEventListener('change', () => { motionRows.style.display = effectSel.value === 'motionPath' ? '' : 'none'; });
992
-
993
- content.appendChild(sec('Timing'));
994
- content.appendChild(field('Duration (s)', 'duration', durationInp));
995
- content.appendChild(field('Ease', 'ease', easeSel));
996
- content.appendChild(field('Repeat', 'repeat', repeatInp));
997
- content.appendChild(yoyoChk);
998
-
999
- const originRows = document.createElement('div');
1000
- originRows.style.display = (cfg.effect === 'rotation' || cfg.effect === 'scale') ? '' : 'none';
1001
- originRows.appendChild(sec('Transform Origin'));
1002
- originRows.appendChild(field('Origin X (%)', 'transformOriginX', originXInp));
1003
- originRows.appendChild(field('Origin Y (%)', 'transformOriginY', originYInp));
1004
- content.appendChild(originRows);
1005
- effectSel.addEventListener('change', () => { originRows.style.display = (effectSel.value === 'rotation' || effectSel.value === 'scale') ? '' : 'none'; });
1006
-
1007
- content.appendChild(sec('Controls (OID triggers)'));
1008
- content.appendChild(playCtrl);
1009
- content.appendChild(pauseCtrl);
1010
- content.appendChild(resumeCtrl);
1011
- content.appendChild(stopCtrl);
1012
- content.appendChild(reverseCtrl);
1051
+ content.appendChild(addBtn);
1013
1052
  }
1014
1053
 
1015
1054
  // ─── Effects Panel ───────────────────────────────────────────────────────
@@ -320,12 +320,19 @@ export async function scanAndApplyAnimations(root) {
320
320
  const elements = (root || document).querySelectorAll('[data-animation]');
321
321
  for (const el of elements) {
322
322
  try {
323
- const cfg = JSON.parse(el.getAttribute('data-animation'));
324
- const existing = _activeAnimations.get(el);
325
- if (existing) existing.destroy();
326
- const inst = new AnimationInstance(el, cfg);
327
- await inst.init();
328
- _activeAnimations.set(el, inst);
323
+ const raw = JSON.parse(el.getAttribute('data-animation'));
324
+ // Support both single object and array of animations
325
+ const cfgList = Array.isArray(raw) ? raw : [raw];
326
+ const existing = _activeAnimations.get(el) || [];
327
+ for (const inst of existing) inst.destroy();
328
+ const instances = [];
329
+ for (const cfg of cfgList) {
330
+ if (!cfg || typeof cfg !== 'object') continue;
331
+ const inst = new AnimationInstance(el, cfg);
332
+ await inst.init();
333
+ instances.push(inst);
334
+ }
335
+ _activeAnimations.set(el, instances);
329
336
  } catch (e) {
330
337
  console.warn('[AnimationService] data-animation parse error on element:', el, e);
331
338
  }
@@ -335,8 +342,9 @@ export async function scanAndApplyAnimations(root) {
335
342
  export function cleanupAnimations(root) {
336
343
  const elements = (root || document).querySelectorAll('[data-animation]');
337
344
  for (const el of elements) {
338
- const inst = _activeAnimations.get(el);
339
- if (inst) { inst.destroy(); _activeAnimations.delete(el); }
345
+ const instances = _activeAnimations.get(el) || [];
346
+ for (const inst of instances) inst.destroy();
347
+ _activeAnimations.delete(el);
340
348
  }
341
349
  }
342
350