draply-dev 1.5.1 → 1.5.3

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.
Files changed (3) hide show
  1. package/bin/cli.js +5 -2
  2. package/package.json +1 -1
  3. package/src/overlay.js +1024 -1016
package/src/overlay.js CHANGED
@@ -824,1105 +824,1113 @@
824
824
  const subLay = document.getElementById('__sub_lay__');
825
825
  if (subLay) subLay.classList.remove('v');
826
826
  }
827
- }
827
+
828
828
 
829
829
  // ── HOVER ────────────────────────────────────────────────────────────────
830
830
  document.addEventListener('mouseover', e => {
831
- if (!state.tool || ps(e.target)) return;
832
- document.querySelectorAll('.__ph__').forEach(el => el.classList.remove('__ph__'));
833
- e.target.classList.add('__ph__');
834
- if (state.tool === 'ins') {
835
- const r = e.target.getBoundingClientRect(), cs = getComputedStyle(e.target);
836
- tip.textContent = `${Math.round(r.width)}×${Math.round(r.height)} ${cs.position} ${cs.display}`;
837
- tip.classList.add('v');
838
- }
839
- });
840
- document.addEventListener('mouseout', e => {
841
- if (!ps(e.target)) e.target.classList.remove('__ph__');
842
- if (state.tool === 'ins') tip.classList.remove('v');
843
- });
844
- document.addEventListener('mousemove', e => {
845
- if (state.tool === 'ins') { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
846
- if (state.dragging || state.resizing) { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
847
- });
848
-
849
- // ── CLICK SELECT ─────────────────────────────────────────────────────────
850
- document.addEventListener('click', e => {
851
- if (!state.tool || ps(e.target) || state.tool === 'ins') return;
852
- e.preventDefault(); e.stopPropagation();
853
-
854
- if (state.tool === 'mov' && e.ctrlKey) {
855
- // Ctrl+click: toggle element in multi-select
856
- const t = e.target;
857
- const idx = state.selectedEls.indexOf(t);
858
- if (idx >= 0) {
859
- // Already in selection — remove it
860
- state.selectedEls.splice(idx, 1);
861
- t.classList.remove('__ps_multi__');
862
- t.classList.remove('__ps__');
863
- // If we removed the primary, promote the first remaining to primary
864
- if (t === state.selectedEl) {
865
- state.selectedEl = state.selectedEls[0] || null;
866
- if (state.selectedEl) {
867
- state.selectedEl.classList.remove('__ps_multi__');
868
- state.selectedEl.classList.add('__ps__');
869
- }
870
- }
871
- } else {
872
- // Add to selection
873
- state.selectedEls.push(t);
874
- t.classList.add('__ps_multi__');
875
- // If no primary yet, make this primary
876
- if (!state.selectedEl) {
877
- state.selectedEl = t;
878
- t.classList.remove('__ps_multi__');
879
- t.classList.add('__ps__');
831
+ if (!state.tool || ps(e.target)) return;
832
+ document.querySelectorAll('.__ph__').forEach(el => el.classList.remove('__ph__'));
833
+ e.target.classList.add('__ph__');
834
+ if (state.tool === 'ins') {
835
+ const r = e.target.getBoundingClientRect(), cs = getComputedStyle(e.target);
836
+ tip.textContent = `${Math.round(r.width)}×${Math.round(r.height)} ${cs.position} ${cs.display}`;
837
+ tip.classList.add('v');
838
+ }
839
+ });
840
+ document.addEventListener('mouseout', e => {
841
+ if (!ps(e.target)) e.target.classList.remove('__ph__');
842
+ if (state.tool === 'ins') tip.classList.remove('v');
843
+ });
844
+ document.addEventListener('mousemove', e => {
845
+ if (state.tool === 'ins') { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
846
+ if (state.dragging || state.resizing) { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
847
+ });
848
+
849
+ // ── CLICK SELECT ─────────────────────────────────────────────────────────
850
+ document.addEventListener('click', e => {
851
+ if (!state.tool || ps(e.target) || state.tool === 'ins') return;
852
+ e.preventDefault(); e.stopPropagation();
853
+
854
+ if (state.tool === 'mov' && e.ctrlKey) {
855
+ // Ctrl+click: toggle element in multi-select
856
+ const t = e.target;
857
+ const idx = state.selectedEls.indexOf(t);
858
+ if (idx >= 0) {
859
+ // Already in selection — remove it
860
+ state.selectedEls.splice(idx, 1);
861
+ t.classList.remove('__ps_multi__');
862
+ t.classList.remove('__ps__');
863
+ // If we removed the primary, promote the first remaining to primary
864
+ if (t === state.selectedEl) {
865
+ state.selectedEl = state.selectedEls[0] || null;
866
+ if (state.selectedEl) {
867
+ state.selectedEl.classList.remove('__ps_multi__');
868
+ state.selectedEl.classList.add('__ps__');
880
869
  }
881
870
  }
882
- // Show handle on primary if any selected
883
- if (state.selectedEl) placeHdl(state.selectedEl);
884
- else hdl.classList.remove('v');
885
- return;
871
+ } else {
872
+ // Add to selection
873
+ state.selectedEls.push(t);
874
+ t.classList.add('__ps_multi__');
875
+ // If no primary yet, make this primary
876
+ if (!state.selectedEl) {
877
+ state.selectedEl = t;
878
+ t.classList.remove('__ps_multi__');
879
+ t.classList.add('__ps__');
880
+ }
886
881
  }
887
-
888
- // Normal click (no Ctrl): clear all and select just this
889
- document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el => {
890
- el.classList.remove('__ps__');
891
- el.classList.remove('__ps_multi__');
892
- });
893
- state.selectedEls = [e.target];
894
- state.selectedEl = e.target;
895
- e.target.classList.add('__ps__');
896
-
897
- if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
898
- if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
899
- if (state.tool === 'clr') populateColors(e.target);
900
- if (state.tool === 'typ') populateTypo(e.target);
901
- }, true);
902
-
903
- // ══════════════════════════════════════════
904
- // MOVE
905
- // ══════════════════════════════════════════
906
- function placeHdl(el) {
907
- const r = el.getBoundingClientRect();
908
- const hdlW = 80; // approximate handle width
909
- // Center above element, but clamp inside viewport
910
- let left = r.left + r.width / 2 - hdlW / 2;
911
- let top = r.top - 36;
912
- // Clamp horizontally
913
- left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
914
- // If element is at top, show handle below instead
915
- if (top < 8) top = r.bottom + 8;
916
- hdl.style.left = left + 'px';
917
- hdl.style.top = top + 'px';
918
- hdl.classList.add('v');
882
+ // Show handle on primary if any selected
883
+ if (state.selectedEl) placeHdl(state.selectedEl);
884
+ else hdl.classList.remove('v');
885
+ return;
919
886
  }
920
887
 
921
- let dEl;
922
- hdl.addEventListener('mousedown', e => {
923
- if (!state.selectedEl) return;
924
- e.preventDefault();
925
- dEl = state.selectedEl;
926
- state.dragSX = e.clientX; state.dragSY = e.clientY;
927
-
928
- // Snapshot original positions for ALL selected elements
929
- const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
930
- state.dragOrigPositions = els.map(el => {
931
- const cs = getComputedStyle(el);
932
- const origL = parseFloat(cs.left) || 0;
933
- const origT = parseFloat(cs.top) || 0;
934
- if (cs.position === 'static') el.style.position = 'relative';
935
- el.style.transition = 'none';
936
- return { el, origL, origT, prevL: origL + 'px', prevT: origT + 'px' };
937
- });
938
-
939
- state.dragging = true;
940
- });
941
- document.addEventListener('mousemove', e => {
942
- if (!state.dragging) return;
943
- const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
944
- state.dragOrigPositions.forEach(({ el, origL, origT }) => {
945
- el.style.left = (origL + dx) + 'px';
946
- el.style.top = (origT + dy) + 'px';
947
- });
948
- if (dEl) {
949
- placeHdl(dEl);
950
- const { origL, origT } = state.dragOrigPositions[0];
951
- tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
952
- tip.classList.add('v');
953
- }
888
+ // Normal click (no Ctrl): clear all and select just this
889
+ document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el => {
890
+ el.classList.remove('__ps__');
891
+ el.classList.remove('__ps_multi__');
954
892
  });
955
- document.addEventListener('mouseup', () => {
956
- if (!state.dragging) return;
957
- state.dragging = false; tip.classList.remove('v');
958
- // Record each element separately
959
- state.dragOrigPositions.forEach(({ el, prevL, prevT }) => {
960
- const cs = getComputedStyle(el);
961
- const newLeft = Math.round(parseFloat(cs.left)) + 'px';
962
- const newTop = Math.round(parseFloat(cs.top)) + 'px';
963
- if (newLeft !== prevL || newTop !== prevT) {
964
- rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
965
- }
966
- });
967
- state.dragOrigPositions = [];
968
- dEl = null;
893
+ state.selectedEls = [e.target];
894
+ state.selectedEl = e.target;
895
+ e.target.classList.add('__ps__');
896
+
897
+ if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
898
+ if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
899
+ if (state.tool === 'clr') populateColors(e.target);
900
+ if (state.tool === 'typ') populateTypo(e.target);
901
+ }, true);
902
+
903
+ // ══════════════════════════════════════════
904
+ // MOVE
905
+ // ══════════════════════════════════════════
906
+ function placeHdl(el) {
907
+ const r = el.getBoundingClientRect();
908
+ const hdlW = 80; // approximate handle width
909
+ // Center above element, but clamp inside viewport
910
+ let left = r.left + r.width / 2 - hdlW / 2;
911
+ let top = r.top - 36;
912
+ // Clamp horizontally
913
+ left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
914
+ // If element is at top, show handle below instead
915
+ if (top < 8) top = r.bottom + 8;
916
+ hdl.style.left = left + 'px';
917
+ hdl.style.top = top + 'px';
918
+ hdl.classList.add('v');
919
+ }
920
+
921
+ let dEl;
922
+ hdl.addEventListener('mousedown', e => {
923
+ if (!state.selectedEl) return;
924
+ e.preventDefault();
925
+ dEl = state.selectedEl;
926
+ state.dragSX = e.clientX; state.dragSY = e.clientY;
927
+
928
+ // Snapshot original positions for ALL selected elements
929
+ const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
930
+ state.dragOrigPositions = els.map(el => {
931
+ const cs = getComputedStyle(el);
932
+ const origL = parseFloat(cs.left) || 0;
933
+ const origT = parseFloat(cs.top) || 0;
934
+ if (cs.position === 'static') el.style.position = 'relative';
935
+ el.style.transition = 'none';
936
+ return { el, origL, origT, prevL: origL + 'px', prevT: origT + 'px' };
969
937
  });
970
938
 
971
- // ══════════════════════════════════════════
972
- // RESIZE
973
- // ══════════════════════════════════════════
974
- function placeRH(el) {
975
- const r = el.getBoundingClientRect();
976
- // Use viewport coords (fixed position handles need viewport coords, not page)
977
- const t = r.top, l = r.left;
978
- const hw = r.width, hh = r.height;
979
- const hs = 5; // half handle size for centering
980
-
981
- const pos = {
982
- nw: [l - hs, t - hs],
983
- n: [l + hw / 2 - hs, t - hs],
984
- ne: [l + hw - hs, t - hs],
985
- w: [l - hs, t + hh / 2 - hs],
986
- e: [l + hw - hs, t + hh / 2 - hs],
987
- sw: [l - hs, t + hh - hs],
988
- s: [l + hw / 2 - hs, t + hh - hs],
989
- se: [l + hw - hs, t + hh - hs],
990
- };
991
- rhDirs.forEach(d => {
992
- rhs[d].style.left = pos[d][0] + 'px';
993
- rhs[d].style.top = pos[d][1] + 'px';
994
- rhs[d].classList.add('v');
995
- });
996
- }
997
-
998
- let rzEl = null, rzDir = null;
999
- rhDirs.forEach(d => {
1000
- rhs[d].addEventListener('mousedown', e => {
1001
- if (!state.selectedEl) return;
1002
- e.preventDefault(); e.stopPropagation();
1003
- rzEl = state.selectedEl; rzDir = d;
1004
- const r = rzEl.getBoundingClientRect();
1005
- const cs = getComputedStyle(rzEl);
1006
- state.resizeOrigW = r.width; state.resizeOrigH = r.height;
1007
- state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
1008
- state.resizeOrigL = parseFloat(cs.left) || 0;
1009
- state.resizeOrigT = parseFloat(cs.top) || 0;
1010
- if (cs.position === 'static') rzEl.style.position = 'relative';
1011
- rzEl.style.transition = 'none';
1012
- state.resizing = true;
1013
- });
939
+ state.dragging = true;
940
+ });
941
+ document.addEventListener('mousemove', e => {
942
+ if (!state.dragging) return;
943
+ const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
944
+ state.dragOrigPositions.forEach(({ el, origL, origT }) => {
945
+ el.style.left = (origL + dx) + 'px';
946
+ el.style.top = (origT + dy) + 'px';
1014
947
  });
1015
-
1016
- document.addEventListener('mousemove', e => {
1017
- if (!state.resizing || !rzEl) return;
1018
- const dx = e.clientX - state.resizeOrigX, dy = e.clientY - state.resizeOrigY;
1019
- let w = state.resizeOrigW, h = state.resizeOrigH;
1020
- let l = state.resizeOrigL, t = state.resizeOrigT;
1021
-
1022
- if (rzDir.includes('e')) w = Math.max(20, w + dx);
1023
- if (rzDir.includes('s')) h = Math.max(20, h + dy);
1024
- if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
1025
- if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
1026
-
1027
- rzEl.style.width = Math.round(w) + 'px';
1028
- rzEl.style.height = Math.round(h) + 'px';
1029
- rzEl.style.left = Math.round(l) + 'px';
1030
- rzEl.style.top = Math.round(t) + 'px';
1031
- placeRH(rzEl);
1032
- tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
948
+ if (dEl) {
949
+ placeHdl(dEl);
950
+ const { origL, origT } = state.dragOrigPositions[0];
951
+ tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
1033
952
  tip.classList.add('v');
1034
- });
1035
-
1036
- document.addEventListener('mouseup', () => {
1037
- if (!state.resizing || !rzEl) return;
1038
- state.resizing = false; tip.classList.remove('v');
1039
- const cs = getComputedStyle(rzEl);
1040
- // prevProps = original values before resize started
1041
- const prevProps = {
1042
- width: Math.round(state.resizeOrigW) + 'px',
1043
- height: Math.round(state.resizeOrigH) + 'px',
1044
- left: Math.round(state.resizeOrigL) + 'px',
1045
- top: Math.round(state.resizeOrigT) + 'px',
1046
- };
1047
- const newSize = {
1048
- width: Math.round(parseFloat(cs.width)) + 'px',
1049
- height: Math.round(parseFloat(cs.height)) + 'px',
1050
- left: Math.round(parseFloat(cs.left)) + 'px',
1051
- top: Math.round(parseFloat(cs.top)) + 'px',
1052
- };
1053
- if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
1054
- rec(rzEl, newSize, prevProps);
1055
- }
1056
- rzEl = null; rzDir = null;
1057
- });
1058
-
1059
- // ══════════════════════════════════════════
1060
- // COLORS
1061
- // ══════════════════════════════════════════
1062
- const clrElName = document.getElementById('__clr_el_name__');
1063
- const swBg = document.getElementById('__sw_bg__'), cpBg = document.getElementById('__cp_bg__');
1064
- const swFg = document.getElementById('__sw_fg__'), cpFg = document.getElementById('__cp_fg__');
1065
- const swBd = document.getElementById('__sw_bd__'), cpBd = document.getElementById('__cp_bd__');
1066
- const cpBgTrans = document.getElementById('__cp_bg_trans__');
1067
- const cpBdTrans = document.getElementById('__cp_bd_trans__');
1068
-
1069
- function setupSwatch(sw, cp) {
1070
- sw.onclick = () => cp.click();
1071
- cp.oninput = () => { sw.style.background = cp.value; };
1072
953
  }
1073
- setupSwatch(swBg, cpBg);
1074
- setupSwatch(swFg, cpFg);
1075
- setupSwatch(swBd, cpBd);
1076
-
1077
- // Transparent checkbox toggles swatch appearance
1078
- cpBgTrans.onchange = () => {
1079
- swBg.style.background = cpBgTrans.checked
1080
- ? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
1081
- : cpBg.value;
1082
- swBg.style.opacity = cpBgTrans.checked ? '0.6' : '1';
1083
- };
1084
- cpBdTrans.onchange = () => {
1085
- swBd.style.background = cpBdTrans.checked
1086
- ? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
1087
- : cpBd.value;
1088
- swBd.style.opacity = cpBdTrans.checked ? '0.6' : '1';
1089
- };
1090
-
1091
- function isTransparent(color) {
1092
- return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
1093
- }
1094
-
1095
- function populateColors(el) {
954
+ });
955
+ document.addEventListener('mouseup', () => {
956
+ if (!state.dragging) return;
957
+ state.dragging = false; tip.classList.remove('v');
958
+ // Record each element separately
959
+ state.dragOrigPositions.forEach(({ el, prevL, prevT }) => {
1096
960
  const cs = getComputedStyle(el);
1097
- clrElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList][0] : '');
1098
- let bg = cs.backgroundColor;
1099
- let fg = cs.color || '#000000';
1100
- let bd = cs.borderColor || '#cccccc';
1101
- const bw = cs.borderWidth;
1102
-
1103
- // #14: Support SVG fill/stroke (#14)
1104
- const isSvg = el.namespaceURI === 'http://www.w3.org/2000/svg';
1105
- if (isSvg) {
1106
- fg = cs.fill;
1107
- bd = cs.stroke;
1108
- }
1109
-
1110
- // Background — detect transparent
1111
- if (isTransparent(bg)) {
1112
- cpBgTrans.checked = true;
1113
- swBg.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
1114
- swBg.style.opacity = '0.6';
1115
- } else {
1116
- cpBgTrans.checked = false;
1117
- cpBg.value = rgb2hex(bg);
1118
- swBg.style.background = cpBg.value;
1119
- swBg.style.opacity = '1';
1120
- }
1121
- cpFg.value = rgb2hex(fg); swFg.style.background = cpFg.value;
1122
- // Border — detect none/transparent
1123
- if (isTransparent(bd) || bw === '0px') {
1124
- cpBdTrans.checked = true;
1125
- swBd.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
1126
- swBd.style.opacity = '0.6';
1127
- } else {
1128
- cpBdTrans.checked = false;
1129
- cpBd.value = rgb2hex(bd);
1130
- swBd.style.background = cpBd.value;
1131
- swBd.style.opacity = '1';
961
+ const newLeft = Math.round(parseFloat(cs.left)) + 'px';
962
+ const newTop = Math.round(parseFloat(cs.top)) + 'px';
963
+ if (newLeft !== prevL || newTop !== prevT) {
964
+ rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
1132
965
  }
1133
-
1134
- // #11: Advanced styles detection
1135
- const opVal = parseFloat(cs.opacity) || 1;
1136
- document.getElementById('__st_opacity__').value = opVal;
1137
- document.getElementById('__st_opacity_val__').textContent = opVal.toFixed(2);
1138
- document.getElementById('__st_radius__').value = parseInt(cs.borderRadius) || 0;
1139
- document.getElementById('__st_shadow__').value = cs.boxShadow === 'none' ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'; // simple match
1140
- }
1141
-
1142
- // #11: Live preview for advanced styles
1143
- document.getElementById('__st_opacity__').oninput = e => {
1144
- const v = e.target.value;
1145
- document.getElementById('__st_opacity_val__').textContent = parseFloat(v).toFixed(2);
1146
- if (state.selectedEl) state.selectedEl.style.opacity = v;
966
+ });
967
+ state.dragOrigPositions = [];
968
+ dEl = null;
969
+ });
970
+
971
+ // ══════════════════════════════════════════
972
+ // RESIZE
973
+ // ══════════════════════════════════════════
974
+ function placeRH(el) {
975
+ const r = el.getBoundingClientRect();
976
+ // Use viewport coords (fixed position handles need viewport coords, not page)
977
+ const t = r.top, l = r.left;
978
+ const hw = r.width, hh = r.height;
979
+ const hs = 5; // half handle size for centering
980
+
981
+ const pos = {
982
+ nw: [l - hs, t - hs],
983
+ n: [l + hw / 2 - hs, t - hs],
984
+ ne: [l + hw - hs, t - hs],
985
+ w: [l - hs, t + hh / 2 - hs],
986
+ e: [l + hw - hs, t + hh / 2 - hs],
987
+ sw: [l - hs, t + hh - hs],
988
+ s: [l + hw / 2 - hs, t + hh - hs],
989
+ se: [l + hw - hs, t + hh - hs],
1147
990
  };
1148
- document.getElementById('__st_radius__').oninput = e => {
1149
- if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
991
+ rhDirs.forEach(d => {
992
+ rhs[d].style.left = pos[d][0] + 'px';
993
+ rhs[d].style.top = pos[d][1] + 'px';
994
+ rhs[d].classList.add('v');
995
+ });
996
+ }
997
+
998
+ let rzEl = null, rzDir = null;
999
+ rhDirs.forEach(d => {
1000
+ rhs[d].addEventListener('mousedown', e => {
1001
+ if (!state.selectedEl) return;
1002
+ e.preventDefault(); e.stopPropagation();
1003
+ rzEl = state.selectedEl; rzDir = d;
1004
+ const r = rzEl.getBoundingClientRect();
1005
+ const cs = getComputedStyle(rzEl);
1006
+ state.resizeOrigW = r.width; state.resizeOrigH = r.height;
1007
+ state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
1008
+ state.resizeOrigL = parseFloat(cs.left) || 0;
1009
+ state.resizeOrigT = parseFloat(cs.top) || 0;
1010
+ if (cs.position === 'static') rzEl.style.position = 'relative';
1011
+ rzEl.style.transition = 'none';
1012
+ state.resizing = true;
1013
+ });
1014
+ });
1015
+
1016
+ document.addEventListener('mousemove', e => {
1017
+ if (!state.resizing || !rzEl) return;
1018
+ const dx = e.clientX - state.resizeOrigX, dy = e.clientY - state.resizeOrigY;
1019
+ let w = state.resizeOrigW, h = state.resizeOrigH;
1020
+ let l = state.resizeOrigL, t = state.resizeOrigT;
1021
+
1022
+ if (rzDir.includes('e')) w = Math.max(20, w + dx);
1023
+ if (rzDir.includes('s')) h = Math.max(20, h + dy);
1024
+ if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
1025
+ if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
1026
+
1027
+ rzEl.style.width = Math.round(w) + 'px';
1028
+ rzEl.style.height = Math.round(h) + 'px';
1029
+ rzEl.style.left = Math.round(l) + 'px';
1030
+ rzEl.style.top = Math.round(t) + 'px';
1031
+ placeRH(rzEl);
1032
+ tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
1033
+ tip.classList.add('v');
1034
+ });
1035
+
1036
+ document.addEventListener('mouseup', () => {
1037
+ if (!state.resizing || !rzEl) return;
1038
+ state.resizing = false; tip.classList.remove('v');
1039
+ const cs = getComputedStyle(rzEl);
1040
+ // prevProps = original values before resize started
1041
+ const prevProps = {
1042
+ width: Math.round(state.resizeOrigW) + 'px',
1043
+ height: Math.round(state.resizeOrigH) + 'px',
1044
+ left: Math.round(state.resizeOrigL) + 'px',
1045
+ top: Math.round(state.resizeOrigT) + 'px',
1150
1046
  };
1151
- document.getElementById('__st_shadow__').onchange = e => {
1152
- if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
1047
+ const newSize = {
1048
+ width: Math.round(parseFloat(cs.width)) + 'px',
1049
+ height: Math.round(parseFloat(cs.height)) + 'px',
1050
+ left: Math.round(parseFloat(cs.left)) + 'px',
1051
+ top: Math.round(parseFloat(cs.top)) + 'px',
1153
1052
  };
1053
+ if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
1054
+ rec(rzEl, newSize, prevProps);
1154
1055
  }
1155
-
1156
- document.getElementById('__clr_apply__').onclick = () => {
1157
- if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
1158
- const el = state.selectedEl;
1159
- const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
1160
- const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
1161
- const opVal = document.getElementById('__st_opacity__').value;
1162
- const radVal = document.getElementById('__st_radius__').value + 'px';
1163
- const shVal = document.getElementById('__st_shadow__').value;
1164
-
1165
- const prevProps = {
1166
- 'background-color': el.style.backgroundColor || '',
1167
- 'color': el.style.color || '',
1168
- 'border': el.style.border || '',
1169
- 'opacity': el.style.opacity || '',
1170
- 'border-radius': el.style.borderRadius || '',
1171
- 'box-shadow': el.style.boxShadow || ''
1172
- };
1173
-
1174
- if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
1175
- el.style.fill = cpFg.value;
1176
- el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
1177
- }
1178
-
1179
- el.style.backgroundColor = bgVal;
1180
- el.style.color = cpFg.value;
1181
- el.style.opacity = opVal;
1182
- el.style.borderRadius = radVal;
1183
- el.style.boxShadow = shVal === 'none' ? '' : shVal;
1184
-
1185
- if (cpBdTrans.checked) {
1186
- el.style.border = 'none';
1187
- } else {
1188
- el.style.borderColor = bdVal;
1189
- if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
1190
- if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
1191
- }
1192
- rec(el, {
1193
- 'background-color': bgVal, color: cpFg.value,
1194
- 'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
1195
- 'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
1196
- }, prevProps);
1197
- toast('🎨 Styles applied!');
1198
- };
1199
-
1200
- function rgb2hex(rgb) {
1201
- if (rgb.startsWith('#')) return rgb;
1202
- const m = rgb.match(/\d+/g);
1203
- if (!m || m.length < 3) return '#000000';
1204
- return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
1056
+ rzEl = null; rzDir = null;
1057
+ });
1058
+
1059
+ // ══════════════════════════════════════════
1060
+ // COLORS
1061
+ // ══════════════════════════════════════════
1062
+ const clrElName = document.getElementById('__clr_el_name__');
1063
+ const swBg = document.getElementById('__sw_bg__'), cpBg = document.getElementById('__cp_bg__');
1064
+ const swFg = document.getElementById('__sw_fg__'), cpFg = document.getElementById('__cp_fg__');
1065
+ const swBd = document.getElementById('__sw_bd__'), cpBd = document.getElementById('__cp_bd__');
1066
+ const cpBgTrans = document.getElementById('__cp_bg_trans__');
1067
+ const cpBdTrans = document.getElementById('__cp_bd_trans__');
1068
+
1069
+ function setupSwatch(sw, cp) {
1070
+ sw.onclick = () => cp.click();
1071
+ cp.oninput = () => { sw.style.background = cp.value; };
1072
+ }
1073
+ setupSwatch(swBg, cpBg);
1074
+ setupSwatch(swFg, cpFg);
1075
+ setupSwatch(swBd, cpBd);
1076
+
1077
+ // Transparent checkbox toggles swatch appearance
1078
+ cpBgTrans.onchange = () => {
1079
+ swBg.style.background = cpBgTrans.checked
1080
+ ? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
1081
+ : cpBg.value;
1082
+ swBg.style.opacity = cpBgTrans.checked ? '0.6' : '1';
1083
+ };
1084
+ cpBdTrans.onchange = () => {
1085
+ swBd.style.background = cpBdTrans.checked
1086
+ ? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
1087
+ : cpBd.value;
1088
+ swBd.style.opacity = cpBdTrans.checked ? '0.6' : '1';
1089
+ };
1090
+
1091
+ function isTransparent(color) {
1092
+ return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
1093
+ }
1094
+
1095
+ function populateColors(el) {
1096
+ const cs = getComputedStyle(el);
1097
+ clrElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList][0] : '');
1098
+ let bg = cs.backgroundColor;
1099
+ let fg = cs.color || '#000000';
1100
+ let bd = cs.borderColor || '#cccccc';
1101
+ const bw = cs.borderWidth;
1102
+
1103
+ // #14: Support SVG fill/stroke (#14)
1104
+ const isSvg = el.namespaceURI === 'http://www.w3.org/2000/svg';
1105
+ if (isSvg) {
1106
+ fg = cs.fill;
1107
+ bd = cs.stroke;
1205
1108
  }
1206
1109
 
1207
- // ══════════════════════════════════════════
1208
- // TYPOGRAPHY
1209
- // ══════════════════════════════════════════
1210
- const typElName = document.getElementById('__typ_el_name__');
1211
- const typSz = document.getElementById('__typ_sz__');
1212
- const typLh = document.getElementById('__typ_lh__');
1213
- const typLs = document.getElementById('__typ_ls__');
1214
- const typFont = document.getElementById('__typ_font__');
1215
- const typBold = document.getElementById('__typ_bold__');
1216
- const typItalic = document.getElementById('__typ_italic__');
1217
- const typUnder = document.getElementById('__typ_under__');
1218
- const typStrike = document.getElementById('__typ_strike__');
1219
- const typUpper = document.getElementById('__typ_upper__');
1220
- const typLower = document.getElementById('__typ_lower__');
1221
-
1222
- // Track toggle states
1223
- const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
1224
-
1225
- function setStyleBtn(btn, key, val) {
1226
- typState[key] = val;
1227
- btn.classList.toggle('on', val);
1110
+ // Background — detect transparent
1111
+ if (isTransparent(bg)) {
1112
+ cpBgTrans.checked = true;
1113
+ swBg.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
1114
+ swBg.style.opacity = '0.6';
1115
+ } else {
1116
+ cpBgTrans.checked = false;
1117
+ cpBg.value = rgb2hex(bg);
1118
+ swBg.style.background = cpBg.value;
1119
+ swBg.style.opacity = '1';
1228
1120
  }
1229
-
1230
- typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
1231
- typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
1232
- typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
1233
- typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
1234
- typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
1235
- typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
1236
-
1237
- // Loaded Google Fonts cache
1238
- const loadedFonts = new Set();
1239
- function loadGoogleFont(name) {
1240
- if (!name || loadedFonts.has(name)) return;
1241
- loadedFonts.add(name);
1242
- const link = document.createElement('link');
1243
- link.rel = 'stylesheet';
1244
- link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
1245
- document.head.appendChild(link);
1121
+ cpFg.value = rgb2hex(fg); swFg.style.background = cpFg.value;
1122
+ // Border detect none/transparent
1123
+ if (isTransparent(bd) || bw === '0px') {
1124
+ cpBdTrans.checked = true;
1125
+ swBd.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
1126
+ swBd.style.opacity = '0.6';
1127
+ } else {
1128
+ cpBdTrans.checked = false;
1129
+ cpBd.value = rgb2hex(bd);
1130
+ swBd.style.background = cpBd.value;
1131
+ swBd.style.opacity = '1';
1246
1132
  }
1247
1133
 
1248
- // Live preview when font changes
1249
- typFont.onchange = () => {
1250
- if (typFont.value) loadGoogleFont(typFont.value);
1134
+ // #11: Advanced styles detection
1135
+ const opVal = parseFloat(cs.opacity) || 1;
1136
+ document.getElementById('__st_opacity__').value = opVal;
1137
+ document.getElementById('__st_opacity_val__').textContent = opVal.toFixed(2);
1138
+ document.getElementById('__st_radius__').value = parseInt(cs.borderRadius) || 0;
1139
+ document.getElementById('__st_shadow__').value = cs.boxShadow === 'none' ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'; // simple match
1140
+ }
1141
+
1142
+ // #11: Live preview for advanced styles
1143
+ document.getElementById('__st_opacity__').oninput = e => {
1144
+ const v = e.target.value;
1145
+ document.getElementById('__st_opacity_val__').textContent = parseFloat(v).toFixed(2);
1146
+ if (state.selectedEl) state.selectedEl.style.opacity = v;
1147
+ };
1148
+ document.getElementById('__st_radius__').oninput = e => {
1149
+ if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
1150
+ };
1151
+ document.getElementById('__st_shadow__').onchange = e => {
1152
+ if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
1153
+ };
1154
+
1155
+ document.getElementById('__clr_apply__').onclick = () => {
1156
+ if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
1157
+ const el = state.selectedEl;
1158
+ const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
1159
+ const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
1160
+ const opVal = document.getElementById('__st_opacity__').value;
1161
+ const radVal = document.getElementById('__st_radius__').value + 'px';
1162
+ const shVal = document.getElementById('__st_shadow__').value;
1163
+
1164
+ const prevProps = {
1165
+ 'background-color': el.style.backgroundColor || '',
1166
+ 'color': el.style.color || '',
1167
+ 'border': el.style.border || '',
1168
+ 'opacity': el.style.opacity || '',
1169
+ 'border-radius': el.style.borderRadius || '',
1170
+ 'box-shadow': el.style.boxShadow || ''
1251
1171
  };
1252
1172
 
1253
- function populateTypo(el) {
1254
- // Make text directly editable
1255
- if (!el.isContentEditable) {
1256
- el.contentEditable = 'true';
1257
- el.dataset.origText = el.innerHTML || '';
1258
- el.focus();
1259
-
1260
- const finishEdit = () => {
1261
- el.contentEditable = 'false';
1262
- el.removeEventListener('blur', finishEdit);
1263
- if (el.innerHTML !== el.dataset.origText) {
1264
- rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
1265
- toast('Text updated!');
1266
- }
1267
- };
1268
- el.addEventListener('blur', finishEdit);
1269
- }
1270
-
1271
- const cs = getComputedStyle(el);
1272
- typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
1273
- typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
1274
- typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
1275
- typLs.value = parseFloat(cs.letterSpacing) || 0;
1276
- typFont.value = '';
1277
-
1278
- // Detect current styles
1279
- setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
1280
- setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
1281
- setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
1282
- setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
1283
- setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
1284
- setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
1285
-
1286
- // Live preview on input change
1287
- [typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
1173
+ if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
1174
+ el.style.fill = cpFg.value;
1175
+ el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
1288
1176
  }
1289
1177
 
1290
- function previewTypo(el) {
1291
- el.style.fontSize = typSz.value + 'px';
1292
- el.style.lineHeight = typLh.value;
1293
- el.style.letterSpacing = typLs.value + 'px';
1294
- el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
1295
- el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
1296
- const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
1297
- el.style.setProperty('text-decoration', deco, 'important');
1298
- el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
1299
- if (typFont.value) {
1300
- loadGoogleFont(typFont.value);
1301
- el.style.fontFamily = `'${typFont.value}', sans-serif`;
1302
- }
1178
+ el.style.backgroundColor = bgVal;
1179
+ el.style.color = cpFg.value;
1180
+ el.style.opacity = opVal;
1181
+ el.style.borderRadius = radVal;
1182
+ el.style.boxShadow = shVal === 'none' ? '' : shVal;
1183
+
1184
+ if (cpBdTrans.checked) {
1185
+ el.style.border = 'none';
1186
+ } else {
1187
+ el.style.borderColor = bdVal;
1188
+ if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
1189
+ if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
1303
1190
  }
1304
-
1305
- document.getElementById('__typ_apply__').onclick = () => {
1306
- if (!state.selectedEl) { toast(' Select a text element first'); return; }
1307
- const el = state.selectedEl;
1308
- const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
1309
- const props = {
1310
- 'font-size': typSz.value + 'px',
1311
- 'font-weight': typState.bold ? '700' : '400',
1312
- 'font-style': typState.italic ? 'italic' : 'normal',
1313
- 'text-decoration': deco,
1314
- 'text-transform': typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none',
1315
- 'line-height': typLh.value,
1316
- 'letter-spacing': typLs.value + 'px',
1317
- };
1318
- if (typFont.value) {
1319
- props['font-family'] = `'${typFont.value}', sans-serif`;
1320
- props['googleFont'] = typFont.value;
1321
- }
1322
- // Snapshot BEFORE previewTypo applies styles
1323
- const prevProps = {};
1324
- Object.keys(props).forEach(p => {
1325
- const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1326
- prevProps[p] = el.style[camel] || '';
1327
- });
1328
- previewTypo(el);
1329
- rec(el, props, prevProps);
1330
- toast('📝 Typography applied!');
1331
- };
1332
-
1333
- // ══════════════════════════════════════════
1334
- // ASSETS
1335
- // ══════════════════════════════════════════
1336
- const astDrop = document.getElementById('__ast_drop__');
1337
- const astFile = document.getElementById('__ast_file__');
1338
- const astList = document.getElementById('__ast_list__');
1339
- const astHint = document.getElementById('__ast_hint__');
1340
- const astStore = []; // { id, name, src, url }
1341
- let pendingAsset = null; // asset waiting to be placed
1342
- let placingEl = null; // the ghost element following cursor
1343
- let astDragEl = null; // placed asset being moved
1344
- let astDragSX, astDragSY, astDragOL, astDragOT;
1345
-
1346
- // Upload via click or drop
1347
- astDrop.onclick = () => astFile.click();
1348
- astFile.onchange = e => loadAssets(e.target.files);
1349
- astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
1350
- astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
1351
- astDrop.ondrop = e => {
1352
- e.preventDefault(); astDrop.style.borderColor = '';
1353
- loadAssets(e.dataTransfer.files);
1354
- };
1355
-
1356
- function loadAssets(files) {
1357
- [...files].forEach(file => {
1358
- if (!file.type.startsWith('image/')) return;
1359
- const reader = new FileReader();
1360
- reader.onload = async ev => {
1361
- try {
1362
- const res = await fetch('/draply-upload', {
1363
- method: 'POST',
1364
- body: JSON.stringify({ name: file.name, base64: ev.target.result })
1365
- });
1366
- const data = await res.json();
1367
- if (data.ok) {
1368
- const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
1369
- astStore.push(asset);
1370
- addThumb(asset);
1371
- astHint.style.display = 'block';
1372
- toast('🖼️ ' + file.name + ' loaded');
1373
- } else {
1374
- toast(' Upload failed');
1375
- }
1376
- } catch(e) {
1377
- toast(' Upload failed');
1378
- }
1379
- };
1380
- reader.readAsDataURL(file);
1381
- });
1382
- }
1383
-
1384
- function addThumb(asset) {
1385
- const img = document.createElement('img');
1386
- img.className = 'ps-ast-thumb';
1387
- img.src = asset.src;
1388
- img.title = asset.name;
1389
- img.dataset.assetId = asset.id;
1390
- img.onclick = () => {
1391
- // Deselect all thumbs
1392
- document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
1393
- img.classList.add('active');
1394
- startPlacing(asset);
1191
+ rec(el, {
1192
+ 'background-color': bgVal, color: cpFg.value,
1193
+ 'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
1194
+ 'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
1195
+ }, prevProps);
1196
+ toast('🎨 Styles applied!');
1197
+ };
1198
+
1199
+ function rgb2hex(rgb) {
1200
+ if (rgb.startsWith('#')) return rgb;
1201
+ const m = rgb.match(/\d+/g);
1202
+ if (!m || m.length < 3) return '#000000';
1203
+ return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
1204
+ }
1205
+
1206
+ // ══════════════════════════════════════════
1207
+ // TYPOGRAPHY
1208
+ // ══════════════════════════════════════════
1209
+ const typElName = document.getElementById('__typ_el_name__');
1210
+ const typSz = document.getElementById('__typ_sz__');
1211
+ const typLh = document.getElementById('__typ_lh__');
1212
+ const typLs = document.getElementById('__typ_ls__');
1213
+ const typFont = document.getElementById('__typ_font__');
1214
+ const typBold = document.getElementById('__typ_bold__');
1215
+ const typItalic = document.getElementById('__typ_italic__');
1216
+ const typUnder = document.getElementById('__typ_under__');
1217
+ const typStrike = document.getElementById('__typ_strike__');
1218
+ const typUpper = document.getElementById('__typ_upper__');
1219
+ const typLower = document.getElementById('__typ_lower__');
1220
+
1221
+ // Track toggle states
1222
+ const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
1223
+
1224
+ function setStyleBtn(btn, key, val) {
1225
+ typState[key] = val;
1226
+ btn.classList.toggle('on', val);
1227
+ }
1228
+
1229
+ typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
1230
+ typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
1231
+ typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
1232
+ typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
1233
+ typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
1234
+ typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
1235
+
1236
+ // Loaded Google Fonts cache
1237
+ const loadedFonts = new Set();
1238
+ function loadGoogleFont(name) {
1239
+ if (!name || loadedFonts.has(name)) return;
1240
+ loadedFonts.add(name);
1241
+ const link = document.createElement('link');
1242
+ link.rel = 'stylesheet';
1243
+ link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
1244
+ document.head.appendChild(link);
1245
+ }
1246
+
1247
+ // Live preview when font changes
1248
+ typFont.onchange = () => {
1249
+ if (typFont.value) loadGoogleFont(typFont.value);
1250
+ };
1251
+
1252
+ function populateTypo(el) {
1253
+ // Make text directly editable
1254
+ if (!el.isContentEditable) {
1255
+ el.contentEditable = 'true';
1256
+ el.dataset.origText = el.innerHTML || '';
1257
+ el.focus();
1258
+
1259
+ const finishEdit = () => {
1260
+ el.contentEditable = 'false';
1261
+ el.removeEventListener('blur', finishEdit);
1262
+ if (el.innerHTML !== el.dataset.origText) {
1263
+ rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
1264
+ toast('Text updated!');
1265
+ }
1395
1266
  };
1396
- astList.appendChild(img);
1267
+ el.addEventListener('blur', finishEdit);
1397
1268
  }
1398
1269
 
1399
- // Start placing: attach ghost image to cursor
1400
- function startPlacing(asset) {
1401
- cancelPlacing();
1402
- pendingAsset = asset;
1403
-
1404
- // Create ghost
1405
- placingEl = document.createElement('div');
1406
- placingEl.className = 'ps-asset-placed placing';
1407
- placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
1408
- placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
1409
- document.body.appendChild(placingEl);
1410
-
1411
- toast('Click anywhere on page to place');
1412
- document.addEventListener('mousemove', onPlacingMove);
1413
- document.addEventListener('click', onPlacingClick, true);
1414
- document.addEventListener('keydown', onPlacingCancel);
1270
+ const cs = getComputedStyle(el);
1271
+ typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
1272
+ typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
1273
+ typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
1274
+ typLs.value = parseFloat(cs.letterSpacing) || 0;
1275
+ typFont.value = '';
1276
+
1277
+ // Detect current styles
1278
+ setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
1279
+ setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
1280
+ setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
1281
+ setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
1282
+ setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
1283
+ setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
1284
+
1285
+ // Live preview on input change
1286
+ [typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
1287
+ }
1288
+
1289
+ function previewTypo(el) {
1290
+ el.style.fontSize = typSz.value + 'px';
1291
+ el.style.lineHeight = typLh.value;
1292
+ el.style.letterSpacing = typLs.value + 'px';
1293
+ el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
1294
+ el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
1295
+ const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
1296
+ el.style.setProperty('text-decoration', deco, 'important');
1297
+ el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
1298
+ if (typFont.value) {
1299
+ loadGoogleFont(typFont.value);
1300
+ el.style.fontFamily = `'${typFont.value}', sans-serif`;
1415
1301
  }
1416
-
1417
- function onPlacingMove(e) {
1418
- if (!placingEl) return;
1419
- placingEl.style.left = (e.clientX - 60) + 'px';
1420
- placingEl.style.top = (e.clientY - 60) + 'px';
1302
+ }
1303
+
1304
+ document.getElementById('__typ_apply__').onclick = () => {
1305
+ if (!state.selectedEl) { toast('⚠ Select a text element first'); return; }
1306
+ const el = state.selectedEl;
1307
+ const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
1308
+ const props = {
1309
+ 'font-size': typSz.value + 'px',
1310
+ 'font-weight': typState.bold ? '700' : '400',
1311
+ 'font-style': typState.italic ? 'italic' : 'normal',
1312
+ 'text-decoration': deco,
1313
+ 'text-transform': typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none',
1314
+ 'line-height': typLh.value,
1315
+ 'letter-spacing': typLs.value + 'px',
1316
+ };
1317
+ if (typFont.value) {
1318
+ props['font-family'] = `'${typFont.value}', sans-serif`;
1319
+ props['googleFont'] = typFont.value;
1421
1320
  }
1422
-
1423
- function onPlacingClick(e) {
1424
- if (ps(e.target)) return; // ignore clicks inside our panel
1425
- e.preventDefault(); e.stopPropagation();
1426
- if (!pendingAsset || !placingEl) return;
1427
-
1428
- if (e.shiftKey || e.altKey) {
1429
- // #6: Better insertion targeting
1430
- const target = e.target;
1431
- if (target.tagName.toLowerCase() === 'img') {
1432
- const prevSrc = target.getAttribute('src');
1433
- target.src = pendingAsset.src;
1434
- rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
1435
- toast('🖼️ Image replaced!');
1436
- } else {
1437
- // Option to insert INSIDE clicked container if Alt is held
1438
- if (e.altKey) {
1439
- placeAsset(pendingAsset, 0, 0, 120, 120, target);
1440
- toast('✦ Placed inside ' + target.tagName.toLowerCase());
1321
+ // Snapshot BEFORE previewTypo applies styles
1322
+ const prevProps = {};
1323
+ Object.keys(props).forEach(p => {
1324
+ const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1325
+ prevProps[p] = el.style[camel] || '';
1326
+ });
1327
+ previewTypo(el);
1328
+ rec(el, props, prevProps);
1329
+ toast('📝 Typography applied!');
1330
+ };
1331
+
1332
+ // ══════════════════════════════════════════
1333
+ // ASSETS
1334
+ // ══════════════════════════════════════════
1335
+ const astDrop = document.getElementById('__ast_drop__');
1336
+ const astFile = document.getElementById('__ast_file__');
1337
+ const astList = document.getElementById('__ast_list__');
1338
+ const astHint = document.getElementById('__ast_hint__');
1339
+ const astStore = []; // { id, name, src, url }
1340
+ let pendingAsset = null; // asset waiting to be placed
1341
+ let placingEl = null; // the ghost element following cursor
1342
+ let astDragEl = null; // placed asset being moved
1343
+ let astDragSX, astDragSY, astDragOL, astDragOT;
1344
+
1345
+ // Upload via click or drop
1346
+ astDrop.onclick = () => astFile.click();
1347
+ astFile.onchange = e => loadAssets(e.target.files);
1348
+ astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
1349
+ astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
1350
+ astDrop.ondrop = e => {
1351
+ e.preventDefault(); astDrop.style.borderColor = '';
1352
+ loadAssets(e.dataTransfer.files);
1353
+ };
1354
+
1355
+ function loadAssets(files) {
1356
+ [...files].forEach(file => {
1357
+ if (!file.type.startsWith('image/')) return;
1358
+ const reader = new FileReader();
1359
+ reader.onload = async ev => {
1360
+ try {
1361
+ const res = await fetch('/draply-upload', {
1362
+ method: 'POST',
1363
+ body: JSON.stringify({ name: file.name, base64: ev.target.result })
1364
+ });
1365
+ const data = await res.json();
1366
+ if (data.ok) {
1367
+ const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
1368
+ astStore.push(asset);
1369
+ addThumb(asset);
1370
+ astHint.style.display = 'block';
1371
+ toast('🖼️ ' + file.name + ' loaded');
1441
1372
  } else {
1442
- const cs = getComputedStyle(target);
1443
- const prevBg = cs.backgroundImage;
1444
- target.style.backgroundImage = `url('${pendingAsset.src}')`;
1445
- target.style.backgroundSize = 'cover';
1446
- rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
1447
- toast('🖼️ Background set!');
1373
+ toast('⚠ Upload failed');
1448
1374
  }
1375
+ } catch (e) {
1376
+ toast('⚠ Upload failed');
1449
1377
  }
1378
+ };
1379
+ reader.readAsDataURL(file);
1380
+ });
1381
+ }
1382
+
1383
+ function addThumb(asset) {
1384
+ const img = document.createElement('img');
1385
+ img.className = 'ps-ast-thumb';
1386
+ img.src = asset.src;
1387
+ img.title = asset.name;
1388
+ img.dataset.assetId = asset.id;
1389
+ img.onclick = () => {
1390
+ // Deselect all thumbs
1391
+ document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
1392
+ img.classList.add('active');
1393
+ startPlacing(asset);
1394
+ };
1395
+ astList.appendChild(img);
1396
+ }
1397
+
1398
+ // Start placing: attach ghost image to cursor
1399
+ function startPlacing(asset) {
1400
+ cancelPlacing();
1401
+ pendingAsset = asset;
1402
+
1403
+ // Create ghost
1404
+ placingEl = document.createElement('div');
1405
+ placingEl.className = 'ps-asset-placed placing';
1406
+ placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
1407
+ placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
1408
+ document.body.appendChild(placingEl);
1409
+
1410
+ toast('Click anywhere on page to place');
1411
+ document.addEventListener('mousemove', onPlacingMove);
1412
+ document.addEventListener('click', onPlacingClick, true);
1413
+ document.addEventListener('keydown', onPlacingCancel);
1414
+ }
1415
+
1416
+ function onPlacingMove(e) {
1417
+ if (!placingEl) return;
1418
+ placingEl.style.left = (e.clientX - 60) + 'px';
1419
+ placingEl.style.top = (e.clientY - 60) + 'px';
1420
+ }
1421
+
1422
+ function onPlacingClick(e) {
1423
+ if (ps(e.target)) return; // ignore clicks inside our panel
1424
+ e.preventDefault(); e.stopPropagation();
1425
+ if (!pendingAsset || !placingEl) return;
1426
+
1427
+ if (e.shiftKey || e.altKey) {
1428
+ // #6: Better insertion targeting
1429
+ const target = e.target;
1430
+ if (target.tagName.toLowerCase() === 'img') {
1431
+ const prevSrc = target.getAttribute('src');
1432
+ target.src = pendingAsset.src;
1433
+ rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
1434
+ toast('🖼️ Image replaced!');
1450
1435
  } else {
1451
- // PLACE mode (standard)
1452
- const x = e.clientX - 60;
1453
- const y = e.clientY - 60;
1454
- placeAsset(pendingAsset, x, y, 120, 120);
1436
+ // Option to insert INSIDE clicked container if Alt is held
1437
+ if (e.altKey) {
1438
+ placeAsset(pendingAsset, 0, 0, 120, 120, target);
1439
+ toast('✦ Placed inside ' + target.tagName.toLowerCase());
1440
+ } else {
1441
+ const cs = getComputedStyle(target);
1442
+ const prevBg = cs.backgroundImage;
1443
+ target.style.backgroundImage = `url('${pendingAsset.src}')`;
1444
+ target.style.backgroundSize = 'cover';
1445
+ rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
1446
+ toast('🖼️ Background set!');
1447
+ }
1455
1448
  }
1456
- cancelPlacing();
1457
- }
1449
+ } else {
1450
+ // PLACE mode (standard)
1451
+ const x = e.clientX - 60;
1452
+ const y = e.clientY - 60;
1453
+ placeAsset(pendingAsset, x, y, 120, 120);
1458
1454
  }
1455
+ cancelPlacing();
1456
+ }
1457
+
1458
+ function onPlacingCancel(e) {
1459
+ if (e.key === 'Escape') cancelPlacing();
1460
+ }
1461
+
1462
+ function cancelPlacing() {
1463
+ if (placingEl) { placingEl.remove(); placingEl = null; }
1464
+ pendingAsset = null;
1465
+ document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
1466
+ document.removeEventListener('mousemove', onPlacingMove);
1467
+ document.removeEventListener('click', onPlacingClick, true);
1468
+ document.removeEventListener('keydown', onPlacingCancel);
1469
+ }
1470
+
1471
+ // #6: Place asset permanently on page (optionally inside a parent)
1472
+ function placeAsset(asset, x, y, w, h, parent = document.body) {
1473
+ const wrap = document.createElement('div');
1474
+ wrap.className = 'ps-asset-placed';
1475
+ const uid = 'ps-asset-' + Date.now();
1476
+ wrap.id = uid;
1477
+
1478
+ // If we have a specific parent, use relative positioning if needed
1479
+ const posType = parent === document.body ? 'fixed' : 'absolute';
1480
+ wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
1481
+ wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
1482
+ parent.appendChild(wrap);
1483
+
1484
+
1485
+ // Click to select this placed asset (for z-index control)
1486
+ wrap.addEventListener('click', e => {
1487
+ if (ps(e.target)) return;
1488
+ selectPlaced(wrap);
1489
+ });
1459
1490
 
1460
- function onPlacingCancel(e) {
1461
- if (e.key === 'Escape') cancelPlacing();
1462
- }
1463
-
1464
- function cancelPlacing() {
1465
- if (placingEl) { placingEl.remove(); placingEl = null; }
1466
- pendingAsset = null;
1467
- document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
1468
- document.removeEventListener('mousemove', onPlacingMove);
1469
- document.removeEventListener('click', onPlacingClick, true);
1470
- document.removeEventListener('keydown', onPlacingCancel);
1471
- }
1472
-
1473
- // #6: Place asset permanently on page (optionally inside a parent)
1474
- function placeAsset(asset, x, y, w, h, parent = document.body) {
1475
- const wrap = document.createElement('div');
1476
- wrap.className = 'ps-asset-placed';
1477
- const uid = 'ps-asset-' + Date.now();
1478
- wrap.id = uid;
1479
-
1480
- // If we have a specific parent, use relative positioning if needed
1481
- const posType = parent === document.body ? 'fixed' : 'absolute';
1482
- wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
1483
- wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
1484
- parent.appendChild(wrap);
1485
-
1486
-
1487
- // Click to select this placed asset (for z-index control)
1488
- wrap.addEventListener('click', e => {
1489
- if (ps(e.target)) return;
1490
- selectPlaced(wrap);
1491
- });
1492
-
1493
- // Make placed asset draggable
1494
- wrap.addEventListener('mousedown', e => {
1495
- if (ps(e.target)) return;
1496
- e.preventDefault();
1497
- selectPlaced(wrap);
1498
- astDragEl = wrap;
1499
- astDragSX = e.clientX; astDragSY = e.clientY;
1500
- astDragOL = parseFloat(wrap.style.left) || 0;
1501
- astDragOT = parseFloat(wrap.style.top) || 0;
1502
- wrap.style.cursor = 'grabbing';
1503
- state.dragging = true;
1504
- });
1505
-
1506
- rec(wrap, {
1507
- src: asset.src,
1508
- left: Math.round(x) + 'px',
1509
- top: Math.round(y) + 'px',
1510
- width: w + 'px',
1511
- height: h + 'px',
1512
- 'z-index': '1',
1513
- }, null, true); // true = isCreate
1514
-
1515
- toast('✦ Placed — drag to reposition');
1491
+ // Make placed asset draggable
1492
+ wrap.addEventListener('mousedown', e => {
1493
+ if (ps(e.target)) return;
1494
+ e.preventDefault();
1516
1495
  selectPlaced(wrap);
1517
- return wrap;
1518
- }
1496
+ astDragEl = wrap;
1497
+ astDragSX = e.clientX; astDragSY = e.clientY;
1498
+ astDragOL = parseFloat(wrap.style.left) || 0;
1499
+ astDragOT = parseFloat(wrap.style.top) || 0;
1500
+ wrap.style.cursor = 'grabbing';
1501
+ state.dragging = true;
1502
+ });
1519
1503
 
1520
- // Track selected placed asset for z-index controls
1521
- let selectedPlaced = null;
1522
- const zCtrl = document.getElementById('__ast_zctrl__');
1523
- const zVal = document.getElementById('__z_val__');
1524
- const zFront = document.getElementById('__z_front__');
1525
- const zBack = document.getElementById('__z_back__');
1526
- const zSet = document.getElementById('__z_set__');
1527
-
1528
- function selectPlaced(wrap) {
1529
- // Deselect previous
1530
- if (selectedPlaced) selectedPlaced.style.outline = '';
1531
- selectedPlaced = wrap;
1532
- wrap.style.outline = '2px solid #7fff6e';
1533
- zVal.value = parseInt(wrap.style.zIndex) || 1;
1534
- zCtrl.style.display = 'block';
1504
+ rec(wrap, {
1505
+ src: asset.src,
1506
+ left: Math.round(x) + 'px',
1507
+ top: Math.round(y) + 'px',
1508
+ width: w + 'px',
1509
+ height: h + 'px',
1510
+ 'z-index': '1',
1511
+ }, null, true); // true = isCreate
1512
+
1513
+ toast('✦ Placed — drag to reposition');
1514
+ selectPlaced(wrap);
1515
+ return wrap;
1516
+ }
1517
+
1518
+ // Track selected placed asset for z-index controls
1519
+ let selectedPlaced = null;
1520
+ const zCtrl = document.getElementById('__ast_zctrl__');
1521
+ const zVal = document.getElementById('__z_val__');
1522
+ const zFront = document.getElementById('__z_front__');
1523
+ const zBack = document.getElementById('__z_back__');
1524
+ const zSet = document.getElementById('__z_set__');
1525
+
1526
+ function selectPlaced(wrap) {
1527
+ // Deselect previous
1528
+ if (selectedPlaced) selectedPlaced.style.outline = '';
1529
+ selectedPlaced = wrap;
1530
+ wrap.style.outline = '2px solid #7fff6e';
1531
+ zVal.value = parseInt(wrap.style.zIndex) || 1;
1532
+ zCtrl.style.display = 'block';
1533
+ }
1534
+
1535
+ zFront.onclick = () => {
1536
+ if (!selectedPlaced) return;
1537
+ // Find max z-index of all placed assets
1538
+ const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1539
+ const nz = max + 1;
1540
+ selectedPlaced.style.zIndex = nz;
1541
+ zVal.value = nz;
1542
+ rec(selectedPlaced, { 'z-index': String(nz) });
1543
+ toast('▲ Moved to front (z:' + nz + ')');
1544
+ };
1545
+
1546
+ zBack.onclick = () => {
1547
+ if (!selectedPlaced) return;
1548
+ const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1549
+ const nz = min - 1;
1550
+ selectedPlaced.style.zIndex = nz;
1551
+ zVal.value = nz;
1552
+ rec(selectedPlaced, { 'z-index': String(nz) });
1553
+ toast('▼ Moved to back (z:' + nz + ')');
1554
+ };
1555
+
1556
+ // #12: LAYERS PANEL LOGIC
1557
+ function updateLayers() {
1558
+ const list = document.getElementById('__lay_list__');
1559
+ list.innerHTML = '';
1560
+
1561
+ // Find interesting elements (headers, sections, divs with classes, images)
1562
+ const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
1563
+ const filtered = [...items].filter(el => {
1564
+ if (ps(el)) return false;
1565
+ return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
1566
+ }).slice(0, 50); // limit for performance
1567
+
1568
+ if (!filtered.length) {
1569
+ list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
1570
+ return;
1535
1571
  }
1536
1572
 
1537
- zFront.onclick = () => {
1538
- if (!selectedPlaced) return;
1539
- // Find max z-index of all placed assets
1540
- const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1541
- const nz = max + 1;
1542
- selectedPlaced.style.zIndex = nz;
1543
- zVal.value = nz;
1544
- rec(selectedPlaced, { 'z-index': String(nz) });
1545
- toast('▲ Moved to front (z:' + nz + ')');
1546
- };
1547
-
1548
- zBack.onclick = () => {
1549
- if (!selectedPlaced) return;
1550
- const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
1551
- const nz = min - 1;
1552
- selectedPlaced.style.zIndex = nz;
1553
- zVal.value = nz;
1554
- rec(selectedPlaced, { 'z-index': String(nz) });
1555
- toast('▼ Moved to back (z:' + nz + ')');
1556
- };
1557
-
1558
- // #12: LAYERS PANEL LOGIC
1559
- function updateLayers() {
1560
- const list = document.getElementById('__lay_list__');
1561
- list.innerHTML = '';
1562
-
1563
- // Find interesting elements (headers, sections, divs with classes, images)
1564
- const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
1565
- const filtered = [...items].filter(el => {
1566
- if (ps(el)) return false;
1567
- return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
1568
- }).slice(0, 50); // limit for performance
1569
-
1570
- if (!filtered.length) {
1571
- list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
1572
- return;
1573
+ filtered.forEach(el => {
1574
+ const row = document.createElement('div');
1575
+ row.style.cssText = 'padding:6px 8px;border-radius:4px;background:#1a1a2a;border:1px solid #2a2a3a;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:10px;color:#8080a8';
1576
+ if (state.selectedEl === el) {
1577
+ row.style.borderColor = '#7fff6e';
1578
+ row.style.background = 'rgba(127,255,110,0.1)';
1579
+ row.style.color = '#7fff6e';
1573
1580
  }
1574
-
1575
- filtered.forEach(el => {
1576
- const row = document.createElement('div');
1577
- row.style.cssText = 'padding:6px 8px;border-radius:4px;background:#1a1a2a;border:1px solid #2a2a3a;cursor:pointer;display:flex;align-items:center;gap:8px;font-size:10px;color:#8080a8';
1578
- if (state.selectedEl === el) {
1579
- row.style.borderColor = '#7fff6e';
1580
- row.style.background = 'rgba(127,255,110,0.1)';
1581
- row.style.color = '#7fff6e';
1582
- }
1583
-
1584
- const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
1585
- const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
1586
-
1587
- row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
1588
- row.onclick = () => {
1589
- select(el);
1590
- updateLayers();
1591
- };
1592
- list.appendChild(row);
1593
- });
1594
- }
1595
-
1596
- zSet.onclick = () => {
1597
- if (!selectedPlaced) return;
1598
- const nz = parseInt(zVal.value) || 0;
1599
- selectedPlaced.style.zIndex = nz;
1600
- rec(selectedPlaced, { 'z-index': String(nz) });
1601
- toast('z-index set to ' + nz);
1602
- };
1603
-
1604
- zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
1605
-
1606
- // Drag placed assets
1607
- document.addEventListener('mousemove', e => {
1608
- if (!astDragEl || !state.dragging) return;
1609
- const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
1610
- astDragEl.style.left = (astDragOL + dx) + 'px';
1611
- astDragEl.style.top = (astDragOT + dy) + 'px';
1612
- tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
1613
- tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
1614
- tip.classList.add('v');
1615
- });
1616
1581
 
1617
- document.addEventListener('mouseup', () => {
1618
- if (!astDragEl) return;
1619
- tip.classList.remove('v');
1620
- const cs = getComputedStyle(astDragEl);
1621
- const newLeft = Math.round(parseFloat(cs.left)) + 'px';
1622
- const newTop = Math.round(parseFloat(cs.top)) + 'px';
1623
- if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
1624
- rec(astDragEl, { left: newLeft, top: newTop });
1625
- }
1626
- astDragEl.style.cursor = 'grab';
1627
- astDragEl = null;
1628
- });
1582
+ const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
1583
+ const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
1629
1584
 
1630
- // ══════════════════════════════════════════
1631
- // RECORD + SAVE
1632
- // ══════════════════════════════════════════
1633
- // Full history — every individual action
1634
- const history = [];
1635
-
1636
- function rec(el, props, prevPropsOverride, isCreate = false) {
1637
- const selector = el.dataset.pixelshiftId ? null : gsel(el);
1638
- const key = el.dataset.pixelshiftId || selector;
1639
-
1640
- // Use provided prevProps if given (snapshot taken before style was applied),
1641
- // otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
1642
- const prevProps = prevPropsOverride || {};
1643
- if (!prevPropsOverride) {
1644
- Object.keys(props).forEach(p => {
1645
- const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1646
- prevProps[p] = el.style[camel] || '';
1585
+ row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
1586
+ row.onclick = () => {
1587
+ // Select element (inline logic — no separate select() function)
1588
+ document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el2 => {
1589
+ el2.classList.remove('__ps__', '__ps_multi__');
1647
1590
  });
1648
- }
1591
+ state.selectedEls = [el];
1592
+ state.selectedEl = el;
1593
+ el.classList.add('__ps__');
1594
+ if (state.tool === 'mov') placeHdl(el);
1595
+ if (state.tool === 'rsz') placeRH(el);
1596
+ if (state.tool === 'clr') populateColors(el);
1597
+ if (state.tool === 'typ') populateTypo(el);
1598
+ updateLayers();
1599
+ };
1600
+ list.appendChild(row);
1601
+ });
1602
+ }
1603
+
1604
+ zSet.onclick = () => {
1605
+ if (!selectedPlaced) return;
1606
+ const nz = parseInt(zVal.value) || 0;
1607
+ selectedPlaced.style.zIndex = nz;
1608
+ rec(selectedPlaced, { 'z-index': String(nz) });
1609
+ toast('z-index set to ' + nz);
1610
+ };
1611
+
1612
+ zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
1613
+
1614
+ // Drag placed assets
1615
+ document.addEventListener('mousemove', e => {
1616
+ if (!astDragEl || !state.dragging) return;
1617
+ const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
1618
+ astDragEl.style.left = (astDragOL + dx) + 'px';
1619
+ astDragEl.style.top = (astDragOT + dy) + 'px';
1620
+ tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
1621
+ tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
1622
+ tip.classList.add('v');
1623
+ });
1624
+
1625
+ document.addEventListener('mouseup', () => {
1626
+ if (!astDragEl) return;
1627
+ tip.classList.remove('v');
1628
+ const cs = getComputedStyle(astDragEl);
1629
+ const newLeft = Math.round(parseFloat(cs.left)) + 'px';
1630
+ const newTop = Math.round(parseFloat(cs.top)) + 'px';
1631
+ if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
1632
+ rec(astDragEl, { left: newLeft, top: newTop });
1633
+ }
1634
+ astDragEl.style.cursor = 'grab';
1635
+ astDragEl = null;
1636
+ });
1637
+
1638
+ // ══════════════════════════════════════════
1639
+ // RECORD + SAVE
1640
+ // ══════════════════════════════════════════
1641
+ // Full history — every individual action
1642
+ const history = [];
1643
+
1644
+ function rec(el, props, prevPropsOverride, isCreate = false) {
1645
+ const selector = el.dataset.pixelshiftId ? null : gsel(el);
1646
+ const key = el.dataset.pixelshiftId || selector;
1647
+
1648
+ // Use provided prevProps if given (snapshot taken before style was applied),
1649
+ // otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
1650
+ const prevProps = prevPropsOverride || {};
1651
+ if (!prevPropsOverride) {
1652
+ Object.keys(props).forEach(p => {
1653
+ const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1654
+ prevProps[p] = el.style[camel] || '';
1655
+ });
1656
+ }
1649
1657
 
1650
- // Extract exact file from React Fiber if available
1651
- function getReactSource(element) {
1652
- for (const key in element) {
1653
- if (key.startsWith('__reactFiber$')) {
1654
- let fiber = element[key];
1655
- while (fiber) {
1656
- if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
1657
- fiber = fiber.return;
1658
- }
1658
+ // Extract exact file from React Fiber if available
1659
+ function getReactSource(element) {
1660
+ for (const key in element) {
1661
+ if (key.startsWith('__reactFiber$')) {
1662
+ let fiber = element[key];
1663
+ while (fiber) {
1664
+ if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
1665
+ fiber = fiber.return;
1659
1666
  }
1660
1667
  }
1661
- return null;
1662
1668
  }
1669
+ return null;
1670
+ }
1663
1671
 
1664
- // Merge into state.changes (for save/apply)
1665
- const ch = {
1666
- type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
1667
- isCreate,
1668
- pixelshiftId: el.dataset.pixelshiftId || null,
1669
- selector,
1670
- exactFile: getReactSource(el) || window.location.pathname.replace(/^\//, '') || null, // fallback for vanilla HTML (#7)
1671
- file: el.dataset.pixelshiftFile || null,
1672
- props,
1673
- tagName: el.tagName.toLowerCase(),
1674
- outerHTML: el.outerHTML // Send the whole element for 'create' actions
1675
- };
1676
- const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
1677
- if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
1672
+ // Merge into state.changes (for save/apply)
1673
+ const ch = {
1674
+ type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
1675
+ isCreate,
1676
+ pixelshiftId: el.dataset.pixelshiftId || null,
1677
+ selector,
1678
+ exactFile: getReactSource(el) || window.location.pathname.replace(/^\//, '') || null, // fallback for vanilla HTML (#7)
1679
+ file: el.dataset.pixelshiftFile || null,
1680
+ props,
1681
+ tagName: el.tagName.toLowerCase(),
1682
+ outerHTML: el.outerHTML // Send the whole element for 'create' actions
1683
+ };
1684
+ const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
1685
+ if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
1678
1686
 
1679
- // Deep check: only record if at least one property actually changed
1680
- let changed = isCreate;
1681
- if (!isCreate) {
1682
- for (const p in props) {
1683
- if (String(props[p]) !== String(prevProps[p])) { changed = true; break; }
1684
- }
1687
+ // Deep check: only record if at least one property actually changed
1688
+ let changed = isCreate;
1689
+ if (!isCreate) {
1690
+ for (const p in props) {
1691
+ if (String(props[p]) !== String(prevProps[p])) { changed = true; break; }
1685
1692
  }
1686
- if (!changed) return;
1687
-
1688
- // Push to history
1689
- const hid = Date.now() + Math.random();
1690
- history.push({ hid, el, props, prevProps, selector: key, isCreate });
1691
-
1692
- updateUnsUI();
1693
1693
  }
1694
-
1695
- function updateUnsUI() {
1696
- const n = state.changes.length;
1697
- sv.disabled = n === 0; nb.textContent = history.length;
1698
- uns.style.display = history.length ? 'flex' : 'none';
1699
-
1700
- // Rebuild history list (newest first)
1701
- unsList.innerHTML = '';
1702
- [...history].reverse().forEach(h => {
1703
- const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
1704
- const row = document.createElement('div');
1705
- row.style.cssText = 'display:flex;align-items:flex-start;gap:6px;background:#0d0d1a;border:1px solid #1e1e3a;border-radius:5px;padding:6px 8px;font-size:9px;';
1706
- row.innerHTML = `
1694
+ if (!changed) return;
1695
+
1696
+ // Push to history
1697
+ const hid = Date.now() + Math.random();
1698
+ history.push({ hid, el, props, prevProps, selector: key, isCreate });
1699
+
1700
+ updateUnsUI();
1701
+ }
1702
+
1703
+ function updateUnsUI() {
1704
+ const n = state.changes.length;
1705
+ sv.disabled = n === 0; nb.textContent = history.length;
1706
+ uns.style.display = history.length ? 'flex' : 'none';
1707
+
1708
+ // Rebuild history list (newest first)
1709
+ unsList.innerHTML = '';
1710
+ [...history].reverse().forEach(h => {
1711
+ const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
1712
+ const row = document.createElement('div');
1713
+ row.style.cssText = 'display:flex;align-items:flex-start;gap:6px;background:#0d0d1a;border:1px solid #1e1e3a;border-radius:5px;padding:6px 8px;font-size:9px;';
1714
+ row.innerHTML = `
1707
1715
  <div style="flex:1;overflow:hidden">
1708
1716
  <div style="color:#7fff6e;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${h.selector || 'placed element'}</div>
1709
1717
  <div style="color:#555577;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${propStr}</div>
1710
1718
  </div>
1711
1719
  <button data-hid="${h.hid}" style="background:none;border:1px solid #2a2a44;color:#555577;border-radius:3px;padding:2px 6px;font-size:9px;cursor:pointer;white-space:nowrap;flex-shrink:0">↩ revert</button>
1712
1720
  `;
1713
- row.querySelector('button').onclick = () => revertChange(h);
1714
- unsList.appendChild(row);
1721
+ row.querySelector('button').onclick = () => revertChange(h);
1722
+ unsList.appendChild(row);
1723
+ });
1724
+ }
1725
+
1726
+ function revertChange(h) {
1727
+ if (h.isCreate) {
1728
+ h.el.remove();
1729
+ } else {
1730
+ // Re-apply previous inline values
1731
+ Object.entries(h.prevProps).forEach(([prop, val]) => {
1732
+ const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1733
+ h.el.style[camel] = val;
1715
1734
  });
1735
+ // Also handle text/html
1736
+ if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
1737
+ if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
1716
1738
  }
1717
-
1718
- function revertChange(h) {
1719
- if (h.isCreate) {
1720
- h.el.remove();
1739
+ // Remove from history
1740
+ const idx = history.findIndex(x => x.hid === h.hid);
1741
+ if (idx >= 0) history.splice(idx, 1);
1742
+ // Rebuild state.changes — preserve all original fields (#1, #2)
1743
+ state.changes = [];
1744
+ history.forEach(x => {
1745
+ const key = x.selector;
1746
+ const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
1747
+ if (i >= 0) {
1748
+ Object.assign(state.changes[i].props, x.props);
1721
1749
  } else {
1722
- // Re-apply previous inline values
1723
- Object.entries(h.prevProps).forEach(([prop, val]) => {
1724
- const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
1725
- h.el.style[camel] = val;
1750
+ state.changes.push({
1751
+ type: x.isCreate ? 'create' : 'css',
1752
+ isCreate: x.isCreate || false,
1753
+ selector: key,
1754
+ props: { ...x.props },
1755
+ outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
1756
+ tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
1726
1757
  });
1727
- // Also handle text/html
1728
- if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
1729
- if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
1730
1758
  }
1731
- // Remove from history
1732
- const idx = history.findIndex(x => x.hid === h.hid);
1733
- if (idx >= 0) history.splice(idx, 1);
1734
- // Rebuild state.changes — preserve all original fields (#1, #2)
1735
- state.changes = [];
1736
- history.forEach(x => {
1737
- const key = x.selector;
1738
- const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
1739
- if (i >= 0) {
1740
- Object.assign(state.changes[i].props, x.props);
1741
- } else {
1742
- state.changes.push({
1743
- type: x.isCreate ? 'create' : 'css',
1744
- isCreate: x.isCreate || false,
1745
- selector: key,
1746
- props: { ...x.props },
1747
- outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
1748
- tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
1749
- });
1750
- }
1751
- });
1752
- updateUnsUI();
1753
- toast('↩ Reverted');
1759
+ });
1760
+ updateUnsUI();
1761
+ toast('↩ Reverted');
1762
+ }
1763
+
1764
+ sv.addEventListener('click', async () => {
1765
+ // Check key config status
1766
+ let hasKey = false;
1767
+ let cfgProvider = 'groq';
1768
+ try {
1769
+ const cfgRes = await fetch('/draply-config');
1770
+ const cfg = await cfgRes.json();
1771
+ hasKey = cfg.hasKey;
1772
+ cfgProvider = cfg.provider || 'groq';
1773
+ } catch (e) { }
1774
+
1775
+ if (!hasKey) {
1776
+ // Ask for provider first (#8)
1777
+ const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / ollama):', 'groq');
1778
+ if (!provider) { toast('Save aborted'); return; }
1779
+ const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
1780
+ if (key) {
1781
+ sv.disabled = true; sv.textContent = 'Validating...';
1782
+ try {
1783
+ const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
1784
+ const vData = await vRes.json();
1785
+ if (!vData.valid && provider !== 'ollama') {
1786
+ toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
1787
+ }
1788
+ } catch { /* allow through if validation endpoint unavailable */ }
1789
+ await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
1790
+ sv.disabled = false; sv.textContent = 'Save';
1791
+ } else {
1792
+ toast('Save aborted: API Key required');
1793
+ return;
1794
+ }
1754
1795
  }
1755
1796
 
1756
- sv.addEventListener('click', async () => {
1757
- // Check key config status
1758
- let hasKey = false;
1759
- let cfgProvider = 'groq';
1760
- try {
1761
- const cfgRes = await fetch('/draply-config');
1762
- const cfg = await cfgRes.json();
1763
- hasKey = cfg.hasKey;
1764
- cfgProvider = cfg.provider || 'groq';
1765
- } catch (e) {}
1766
-
1767
- if (!hasKey) {
1768
- // Ask for provider first (#8)
1769
- const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / ollama):', 'groq');
1770
- if (!provider) { toast('Save aborted'); return; }
1771
- const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
1772
- if (key) {
1773
- sv.disabled = true; sv.textContent = 'Validating...';
1774
- try {
1775
- const vRes = await fetch('/draply-validate-key', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
1776
- const vData = await vRes.json();
1777
- if (!vData.valid && provider !== 'ollama') {
1778
- toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
1779
- }
1780
- } catch { /* allow through if validation endpoint unavailable */ }
1781
- await fetch('/draply-config', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
1782
- sv.disabled = false; sv.textContent = 'Save';
1783
- } else {
1784
- toast('Save aborted: API Key required');
1785
- return;
1786
- }
1787
- }
1797
+ // Progress indicator with animation (#9)
1798
+ sv.disabled = true;
1799
+ sv.textContent = '';
1800
+ sv.innerHTML = '<span style="display:inline-flex;align-items:center;gap:4px"><span style="animation:spin 1s linear infinite;display:inline-block">@</span> Applying...</span>';
1801
+ // Add spin keyframe if not exists
1802
+ if (!document.getElementById('__ps_spin_style__')) {
1803
+ const spinStyle = document.createElement('style');
1804
+ spinStyle.id = '__ps_spin_style__';
1805
+ spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
1806
+ document.head.appendChild(spinStyle);
1807
+ }
1808
+ try {
1809
+ const r = await fetch('/draply-ai-apply', {
1810
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
1811
+ body: JSON.stringify({ changes: state.changes })
1812
+ });
1813
+ const d = await r.json();
1814
+ if (d.ok) {
1815
+ const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
1816
+ toast(msg);
1817
+ } else toast('⚠ Error: ' + (d.error || 'unknown'));
1818
+ } catch {
1819
+ toast('⚠ Server unreachable');
1820
+ }
1788
1821
 
1789
- // Progress indicator with animation (#9)
1790
- sv.disabled = true;
1791
- sv.textContent = '';
1792
- sv.innerHTML = '<span style="display:inline-flex;align-items:center;gap:4px"><span style="animation:spin 1s linear infinite;display:inline-block">@</span> Applying...</span>';
1793
- // Add spin keyframe if not exists
1794
- if (!document.getElementById('__ps_spin_style__')) {
1795
- const spinStyle = document.createElement('style');
1796
- spinStyle.id = '__ps_spin_style__';
1797
- spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
1798
- document.head.appendChild(spinStyle);
1799
- }
1800
- try {
1801
- const r = await fetch('/draply-ai-apply', {
1802
- method: 'POST', headers: { 'Content-Type': 'application/json' },
1803
- body: JSON.stringify({ changes: state.changes })
1804
- });
1805
- const d = await r.json();
1806
- if (d.ok) {
1807
- const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
1808
- toast(msg);
1809
- } else toast('⚠ Error: ' + (d.error || 'unknown'));
1810
- } catch {
1811
- toast('⚠ Server unreachable');
1812
- }
1813
-
1814
- state.changes = []; history.length = 0;
1815
- sv.innerHTML = 'Save'; sv.textContent = 'Save';
1816
- updateUnsUI();
1817
- });
1822
+ state.changes = []; history.length = 0;
1823
+ sv.innerHTML = 'Save'; sv.textContent = 'Save';
1824
+ updateUnsUI();
1825
+ });
1818
1826
 
1819
- document.getElementById('__uns_clear__').onclick = () => {
1820
- state.changes = []; history.length = 0; updateUnsUI();
1821
- toast('🗑 History cleared');
1822
- };
1827
+ document.getElementById('__uns_clear__').onclick = () => {
1828
+ state.changes = []; history.length = 0; updateUnsUI();
1829
+ toast('🗑 History cleared');
1830
+ };
1823
1831
 
1824
- document.getElementById('__uns_save__').onclick = () => sv.click();
1832
+ document.getElementById('__uns_save__').onclick = () => sv.click();
1825
1833
 
1826
- // ══════════════════════════════════════════
1827
- // KEYBOARD SHORTCUTS (#4, #5, #15)
1828
- // ══════════════════════════════════════════
1829
- document.addEventListener('keydown', e => {
1830
- // Ignore if typing in an input/textarea/contenteditable
1831
- const tag = e.target.tagName.toLowerCase();
1832
- if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
1834
+ // ══════════════════════════════════════════
1835
+ // KEYBOARD SHORTCUTS (#4, #5, #15)
1836
+ // ══════════════════════════════════════════
1837
+ document.addEventListener('keydown', e => {
1838
+ // Ignore if typing in an input/textarea/contenteditable
1839
+ const tag = e.target.tagName.toLowerCase();
1840
+ if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
1833
1841
 
1834
- // Ctrl+Z — Undo last change (#4)
1835
- if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
1836
- e.preventDefault();
1837
- if (history.length > 0) {
1838
- revertChange(history[history.length - 1]);
1839
- } else {
1840
- toast('Nothing to undo');
1841
- }
1842
- return;
1843
- }
1844
-
1845
- // Ctrl+Shift+Z — Redo (not implemented yet, just prevent default)
1846
- if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') {
1847
- e.preventDefault();
1848
- toast('Redo not available yet');
1849
- return;
1850
- }
1851
-
1852
- // Delete / Backspace — remove selected element (#5)
1853
- if (e.key === 'Delete' || e.key === 'Backspace') {
1854
- if (state.selectedEl && !ps(state.selectedEl)) {
1855
- e.preventDefault();
1856
- const el = state.selectedEl;
1857
- // Record removal in history
1858
- rec(el, { display: 'none' }, { display: el.style.display || '' });
1859
- el.style.display = 'none';
1860
- // Deselect
1861
- el.classList.remove('__ps__', '__ps_multi__');
1862
- state.selectedEl = null;
1863
- state.selectedEls = [];
1864
- hdl.classList.remove('v');
1865
- toast('🗑 Element hidden (Delete)');
1866
- }
1867
- return;
1842
+ // Ctrl+Z — Undo last change (#4)
1843
+ if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
1844
+ e.preventDefault();
1845
+ if (history.length > 0) {
1846
+ revertChange(history[history.length - 1]);
1847
+ } else {
1848
+ toast('Nothing to undo');
1868
1849
  }
1850
+ return;
1851
+ }
1869
1852
 
1870
- // Preview mode toggle: P key (#15)
1871
- if (e.key === 'p' || e.key === 'P') {
1872
- if (!e.ctrlKey && !e.metaKey) {
1873
- e.preventDefault();
1874
- togglePreview();
1875
- }
1876
- return;
1877
- }
1878
- });
1853
+ // Ctrl+Shift+Z Redo (not implemented yet, just prevent default)
1854
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') {
1855
+ e.preventDefault();
1856
+ toast('Redo not available yet');
1857
+ return;
1858
+ }
1879
1859
 
1880
- // ══════════════════════════════════════════
1881
- // PREVIEW MODE (#15)
1882
- // ══════════════════════════════════════════
1883
- let previewMode = false;
1884
- function togglePreview() {
1885
- previewMode = !previewMode;
1886
- const psRoot = document.getElementById('__ps__');
1887
- if (previewMode) {
1888
- psRoot.style.display = 'none';
1889
- // Remove all selection highlights
1890
- document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
1891
- el.classList.remove('__ps__', '__ps_multi__', '__ph__');
1892
- });
1860
+ // Delete / Backspace — remove selected element (#5)
1861
+ if (e.key === 'Delete' || e.key === 'Backspace') {
1862
+ if (state.selectedEl && !ps(state.selectedEl)) {
1863
+ e.preventDefault();
1864
+ const el = state.selectedEl;
1865
+ // Record removal in history
1866
+ rec(el, { display: 'none' }, { display: el.style.display || '' });
1867
+ el.style.display = 'none';
1868
+ // Deselect
1869
+ el.classList.remove('__ps__', '__ps_multi__');
1870
+ state.selectedEl = null;
1871
+ state.selectedEls = [];
1893
1872
  hdl.classList.remove('v');
1894
- Object.values(rhs).forEach(h => h.classList.remove('v'));
1895
- toast('👁 Preview mode — press P to exit');
1896
- } else {
1897
- psRoot.style.display = '';
1898
- toast('Editor restored');
1873
+ toast('🗑 Element hidden (Delete)');
1899
1874
  }
1875
+ return;
1900
1876
  }
1901
1877
 
1902
- // ══════════════════════════════════════════
1903
- // UTILS
1904
- // ══════════════════════════════════════════
1905
- function ps(el) { return el && el.closest('#__ps__'); }
1906
- function gsel(el) {
1907
- if (el.id) return '#' + el.id;
1908
- // Build unique path up the DOM
1909
- const parts = [];
1910
- let cur = el;
1911
- while (cur && cur !== document.body && cur !== document.documentElement) {
1912
- if (cur.id) { parts.unshift('#' + cur.id); break; }
1913
- const tag = cur.tagName.toLowerCase();
1914
- const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
1915
- const siblings = cur.parentElement
1916
- ? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
1917
- : [];
1918
- const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
1919
- parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
1920
- cur = cur.parentElement;
1878
+ // Preview mode toggle: P key (#15)
1879
+ if (e.key === 'p' || e.key === 'P') {
1880
+ if (!e.ctrlKey && !e.metaKey) {
1881
+ e.preventDefault();
1882
+ togglePreview();
1921
1883
  }
1922
- return parts.join(' > ');
1884
+ return;
1885
+ }
1886
+ });
1887
+
1888
+ // ══════════════════════════════════════════
1889
+ // PREVIEW MODE (#15)
1890
+ // ══════════════════════════════════════════
1891
+ let previewMode = false;
1892
+ function togglePreview() {
1893
+ previewMode = !previewMode;
1894
+ const psRoot = document.getElementById('__ps__');
1895
+ if (previewMode) {
1896
+ psRoot.style.display = 'none';
1897
+ // Remove all selection highlights
1898
+ document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
1899
+ el.classList.remove('__ps__', '__ps_multi__', '__ph__');
1900
+ });
1901
+ hdl.classList.remove('v');
1902
+ Object.values(rhs).forEach(h => h.classList.remove('v'));
1903
+ toast('👁 Preview mode — press P to exit');
1904
+ } else {
1905
+ psRoot.style.display = '';
1906
+ toast('Editor restored');
1923
1907
  }
1924
- function toast(msg) {
1925
- tst.textContent = msg; tst.classList.add('v');
1926
- clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
1908
+ }
1909
+
1910
+ // ══════════════════════════════════════════
1911
+ // UTILS
1912
+ // ══════════════════════════════════════════
1913
+ function ps(el) { return el && el.closest('#__ps__'); }
1914
+ function gsel(el) {
1915
+ if (el.id) return '#' + el.id;
1916
+ // Build unique path up the DOM
1917
+ const parts = [];
1918
+ let cur = el;
1919
+ while (cur && cur !== document.body && cur !== document.documentElement) {
1920
+ if (cur.id) { parts.unshift('#' + cur.id); break; }
1921
+ const tag = cur.tagName.toLowerCase();
1922
+ const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
1923
+ const siblings = cur.parentElement
1924
+ ? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
1925
+ : [];
1926
+ const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
1927
+ parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
1928
+ cur = cur.parentElement;
1927
1929
  }
1928
- })();
1930
+ return parts.join(' > ');
1931
+ }
1932
+ function toast(msg) {
1933
+ tst.textContent = msg; tst.classList.add('v');
1934
+ clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
1935
+ }
1936
+ }) ();