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.
- package/bin/cli.js +5 -2
- package/package.json +1 -1
- 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
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
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
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
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
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
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
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
//
|
|
1078
|
-
|
|
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
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1152
|
-
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
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
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
//
|
|
1249
|
-
|
|
1250
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
el.style.
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
}
|
|
1377
|
-
|
|
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
|
-
|
|
1267
|
+
el.addEventListener('blur', finishEdit);
|
|
1397
1268
|
}
|
|
1398
1269
|
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1461
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
1618
|
-
|
|
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
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
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
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
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
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
});
|
|
1752
|
-
|
|
1753
|
-
|
|
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
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
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
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1827
|
+
document.getElementById('__uns_clear__').onclick = () => {
|
|
1828
|
+
state.changes = []; history.length = 0; updateUnsUI();
|
|
1829
|
+
toast('🗑 History cleared');
|
|
1830
|
+
};
|
|
1823
1831
|
|
|
1824
|
-
|
|
1832
|
+
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
1825
1833
|
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
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
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
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
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
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
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
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
|
|
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
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
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
|
+
}) ();
|