draply-dev 1.5.2 → 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 -1015
package/src/overlay.js
CHANGED
|
@@ -824,1104 +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');
|
|
953
|
+
}
|
|
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 }) => {
|
|
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
|
+
}
|
|
1034
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],
|
|
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
|
+
}
|
|
1035
997
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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();
|
|
1039
1005
|
const cs = getComputedStyle(rzEl);
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
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;
|
|
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;
|
|
1057
1013
|
});
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
const
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
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',
|
|
1083
1046
|
};
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
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',
|
|
1089
1052
|
};
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
|
|
1053
|
+
if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
|
|
1054
|
+
rec(rzEl, newSize, prevProps);
|
|
1093
1055
|
}
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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;
|
|
1140
1108
|
}
|
|
1141
1109
|
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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 || ''
|
|
1171
|
-
};
|
|
1172
|
-
|
|
1173
|
-
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1174
|
-
el.style.fill = cpFg.value;
|
|
1175
|
-
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1176
|
-
}
|
|
1177
|
-
|
|
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';
|
|
1190
|
-
}
|
|
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);
|
|
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';
|
|
1227
1120
|
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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);
|
|
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';
|
|
1245
1132
|
}
|
|
1246
1133
|
|
|
1247
|
-
//
|
|
1248
|
-
|
|
1249
|
-
|
|
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 || ''
|
|
1250
1171
|
};
|
|
1251
1172
|
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
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
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
el.addEventListener('blur', finishEdit);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
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); });
|
|
1173
|
+
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1174
|
+
el.style.fill = cpFg.value;
|
|
1175
|
+
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1287
1176
|
}
|
|
1288
1177
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
el.style.
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
}
|
|
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;
|
|
1320
|
-
}
|
|
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');
|
|
1372
|
-
} else {
|
|
1373
|
-
toast('⚠ Upload failed');
|
|
1374
|
-
}
|
|
1375
|
-
} catch(e) {
|
|
1376
|
-
toast('⚠ Upload failed');
|
|
1377
|
-
}
|
|
1378
|
-
};
|
|
1379
|
-
reader.readAsDataURL(file);
|
|
1380
|
-
});
|
|
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';
|
|
1381
1190
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
}
|
|
1394
1266
|
};
|
|
1395
|
-
|
|
1267
|
+
el.addEventListener('blur', finishEdit);
|
|
1396
1268
|
}
|
|
1397
1269
|
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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`;
|
|
1414
1301
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
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;
|
|
1420
1320
|
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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');
|
|
1440
1372
|
} else {
|
|
1441
|
-
|
|
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!');
|
|
1373
|
+
toast('⚠ Upload failed');
|
|
1447
1374
|
}
|
|
1375
|
+
} catch (e) {
|
|
1376
|
+
toast('⚠ Upload failed');
|
|
1448
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!');
|
|
1449
1435
|
} else {
|
|
1450
|
-
//
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
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
|
+
}
|
|
1454
1448
|
}
|
|
1455
|
-
|
|
1456
|
-
|
|
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);
|
|
1457
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
|
+
});
|
|
1458
1490
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
function cancelPlacing() {
|
|
1464
|
-
if (placingEl) { placingEl.remove(); placingEl = null; }
|
|
1465
|
-
pendingAsset = null;
|
|
1466
|
-
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1467
|
-
document.removeEventListener('mousemove', onPlacingMove);
|
|
1468
|
-
document.removeEventListener('click', onPlacingClick, true);
|
|
1469
|
-
document.removeEventListener('keydown', onPlacingCancel);
|
|
1470
|
-
}
|
|
1471
|
-
|
|
1472
|
-
// #6: Place asset permanently on page (optionally inside a parent)
|
|
1473
|
-
function placeAsset(asset, x, y, w, h, parent = document.body) {
|
|
1474
|
-
const wrap = document.createElement('div');
|
|
1475
|
-
wrap.className = 'ps-asset-placed';
|
|
1476
|
-
const uid = 'ps-asset-' + Date.now();
|
|
1477
|
-
wrap.id = uid;
|
|
1478
|
-
|
|
1479
|
-
// If we have a specific parent, use relative positioning if needed
|
|
1480
|
-
const posType = parent === document.body ? 'fixed' : 'absolute';
|
|
1481
|
-
wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
|
|
1482
|
-
wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
|
|
1483
|
-
parent.appendChild(wrap);
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
// Click to select this placed asset (for z-index control)
|
|
1487
|
-
wrap.addEventListener('click', e => {
|
|
1488
|
-
if (ps(e.target)) return;
|
|
1489
|
-
selectPlaced(wrap);
|
|
1490
|
-
});
|
|
1491
|
-
|
|
1492
|
-
// Make placed asset draggable
|
|
1493
|
-
wrap.addEventListener('mousedown', e => {
|
|
1494
|
-
if (ps(e.target)) return;
|
|
1495
|
-
e.preventDefault();
|
|
1496
|
-
selectPlaced(wrap);
|
|
1497
|
-
astDragEl = wrap;
|
|
1498
|
-
astDragSX = e.clientX; astDragSY = e.clientY;
|
|
1499
|
-
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1500
|
-
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1501
|
-
wrap.style.cursor = 'grabbing';
|
|
1502
|
-
state.dragging = true;
|
|
1503
|
-
});
|
|
1504
|
-
|
|
1505
|
-
rec(wrap, {
|
|
1506
|
-
src: asset.src,
|
|
1507
|
-
left: Math.round(x) + 'px',
|
|
1508
|
-
top: Math.round(y) + 'px',
|
|
1509
|
-
width: w + 'px',
|
|
1510
|
-
height: h + 'px',
|
|
1511
|
-
'z-index': '1',
|
|
1512
|
-
}, null, true); // true = isCreate
|
|
1513
|
-
|
|
1514
|
-
toast('✦ Placed — drag to reposition');
|
|
1491
|
+
// Make placed asset draggable
|
|
1492
|
+
wrap.addEventListener('mousedown', e => {
|
|
1493
|
+
if (ps(e.target)) return;
|
|
1494
|
+
e.preventDefault();
|
|
1515
1495
|
selectPlaced(wrap);
|
|
1516
|
-
|
|
1517
|
-
|
|
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
|
+
});
|
|
1518
1503
|
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
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;
|
|
1534
1571
|
}
|
|
1535
1572
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1544
|
-
toast('▲ Moved to front (z:' + nz + ')');
|
|
1545
|
-
};
|
|
1546
|
-
|
|
1547
|
-
zBack.onclick = () => {
|
|
1548
|
-
if (!selectedPlaced) return;
|
|
1549
|
-
const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1550
|
-
const nz = min - 1;
|
|
1551
|
-
selectedPlaced.style.zIndex = nz;
|
|
1552
|
-
zVal.value = nz;
|
|
1553
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1554
|
-
toast('▼ Moved to back (z:' + nz + ')');
|
|
1555
|
-
};
|
|
1556
|
-
|
|
1557
|
-
// #12: LAYERS PANEL LOGIC
|
|
1558
|
-
function updateLayers() {
|
|
1559
|
-
const list = document.getElementById('__lay_list__');
|
|
1560
|
-
list.innerHTML = '';
|
|
1561
|
-
|
|
1562
|
-
// Find interesting elements (headers, sections, divs with classes, images)
|
|
1563
|
-
const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
|
|
1564
|
-
const filtered = [...items].filter(el => {
|
|
1565
|
-
if (ps(el)) return false;
|
|
1566
|
-
return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
|
|
1567
|
-
}).slice(0, 50); // limit for performance
|
|
1568
|
-
|
|
1569
|
-
if (!filtered.length) {
|
|
1570
|
-
list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
|
|
1571
|
-
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';
|
|
1572
1580
|
}
|
|
1573
|
-
|
|
1574
|
-
filtered.forEach(el => {
|
|
1575
|
-
const row = document.createElement('div');
|
|
1576
|
-
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';
|
|
1577
|
-
if (state.selectedEl === el) {
|
|
1578
|
-
row.style.borderColor = '#7fff6e';
|
|
1579
|
-
row.style.background = 'rgba(127,255,110,0.1)';
|
|
1580
|
-
row.style.color = '#7fff6e';
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
|
|
1584
|
-
const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
|
|
1585
|
-
|
|
1586
|
-
row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
|
|
1587
|
-
row.onclick = () => {
|
|
1588
|
-
select(el);
|
|
1589
|
-
updateLayers();
|
|
1590
|
-
};
|
|
1591
|
-
list.appendChild(row);
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
|
|
1595
|
-
zSet.onclick = () => {
|
|
1596
|
-
if (!selectedPlaced) return;
|
|
1597
|
-
const nz = parseInt(zVal.value) || 0;
|
|
1598
|
-
selectedPlaced.style.zIndex = nz;
|
|
1599
|
-
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1600
|
-
toast('z-index set to ' + nz);
|
|
1601
|
-
};
|
|
1602
|
-
|
|
1603
|
-
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1604
|
-
|
|
1605
|
-
// Drag placed assets
|
|
1606
|
-
document.addEventListener('mousemove', e => {
|
|
1607
|
-
if (!astDragEl || !state.dragging) return;
|
|
1608
|
-
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1609
|
-
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1610
|
-
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1611
|
-
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1612
|
-
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1613
|
-
tip.classList.add('v');
|
|
1614
|
-
});
|
|
1615
1581
|
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
tip.classList.remove('v');
|
|
1619
|
-
const cs = getComputedStyle(astDragEl);
|
|
1620
|
-
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1621
|
-
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1622
|
-
if (newLeft !== (astDragOL + 'px') || newTop !== (astDragOT + 'px')) {
|
|
1623
|
-
rec(astDragEl, { left: newLeft, top: newTop });
|
|
1624
|
-
}
|
|
1625
|
-
astDragEl.style.cursor = 'grab';
|
|
1626
|
-
astDragEl = null;
|
|
1627
|
-
});
|
|
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] : '');
|
|
1628
1584
|
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1636
|
-
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1637
|
-
const key = el.dataset.pixelshiftId || selector;
|
|
1638
|
-
|
|
1639
|
-
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1640
|
-
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1641
|
-
const prevProps = prevPropsOverride || {};
|
|
1642
|
-
if (!prevPropsOverride) {
|
|
1643
|
-
Object.keys(props).forEach(p => {
|
|
1644
|
-
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1645
|
-
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__');
|
|
1646
1590
|
});
|
|
1647
|
-
|
|
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
|
+
}
|
|
1648
1657
|
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
}
|
|
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;
|
|
1658
1666
|
}
|
|
1659
1667
|
}
|
|
1660
|
-
return null;
|
|
1661
1668
|
}
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1662
1671
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
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);
|
|
1677
1686
|
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
}
|
|
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; }
|
|
1684
1692
|
}
|
|
1685
|
-
if (!changed) return;
|
|
1686
|
-
|
|
1687
|
-
// Push to history
|
|
1688
|
-
const hid = Date.now() + Math.random();
|
|
1689
|
-
history.push({ hid, el, props, prevProps, selector: key, isCreate });
|
|
1690
|
-
|
|
1691
|
-
updateUnsUI();
|
|
1692
1693
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
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 = `
|
|
1706
1715
|
<div style="flex:1;overflow:hidden">
|
|
1707
1716
|
<div style="color:#7fff6e;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${h.selector || 'placed element'}</div>
|
|
1708
1717
|
<div style="color:#555577;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${propStr}</div>
|
|
1709
1718
|
</div>
|
|
1710
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>
|
|
1711
1720
|
`;
|
|
1712
|
-
|
|
1713
|
-
|
|
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;
|
|
1714
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;
|
|
1715
1738
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
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);
|
|
1720
1749
|
} else {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
|
1725
1757
|
});
|
|
1726
|
-
// Also handle text/html
|
|
1727
|
-
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1728
|
-
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1729
1758
|
}
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
});
|
|
1751
|
-
|
|
1752
|
-
|
|
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
|
+
}
|
|
1753
1795
|
}
|
|
1754
1796
|
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1780
|
-
await fetch('/draply-config', { method: 'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1781
|
-
sv.disabled = false; sv.textContent = 'Save';
|
|
1782
|
-
} else {
|
|
1783
|
-
toast('Save aborted: API Key required');
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
1786
|
-
}
|
|
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
|
+
}
|
|
1787
1821
|
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
// Add spin keyframe if not exists
|
|
1793
|
-
if (!document.getElementById('__ps_spin_style__')) {
|
|
1794
|
-
const spinStyle = document.createElement('style');
|
|
1795
|
-
spinStyle.id = '__ps_spin_style__';
|
|
1796
|
-
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1797
|
-
document.head.appendChild(spinStyle);
|
|
1798
|
-
}
|
|
1799
|
-
try {
|
|
1800
|
-
const r = await fetch('/draply-ai-apply', {
|
|
1801
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1802
|
-
body: JSON.stringify({ changes: state.changes })
|
|
1803
|
-
});
|
|
1804
|
-
const d = await r.json();
|
|
1805
|
-
if (d.ok) {
|
|
1806
|
-
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
1807
|
-
toast(msg);
|
|
1808
|
-
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1809
|
-
} catch {
|
|
1810
|
-
toast('⚠ Server unreachable');
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
state.changes = []; history.length = 0;
|
|
1814
|
-
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1815
|
-
updateUnsUI();
|
|
1816
|
-
});
|
|
1822
|
+
state.changes = []; history.length = 0;
|
|
1823
|
+
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1824
|
+
updateUnsUI();
|
|
1825
|
+
});
|
|
1817
1826
|
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1827
|
+
document.getElementById('__uns_clear__').onclick = () => {
|
|
1828
|
+
state.changes = []; history.length = 0; updateUnsUI();
|
|
1829
|
+
toast('🗑 History cleared');
|
|
1830
|
+
};
|
|
1822
1831
|
|
|
1823
|
-
|
|
1832
|
+
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
1824
1833
|
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
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;
|
|
1832
1841
|
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1841
|
-
return;
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
// Ctrl+Shift+Z — Redo (not implemented yet, just prevent default)
|
|
1845
|
-
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') {
|
|
1846
|
-
e.preventDefault();
|
|
1847
|
-
toast('Redo not available yet');
|
|
1848
|
-
return;
|
|
1849
|
-
}
|
|
1850
|
-
|
|
1851
|
-
// Delete / Backspace — remove selected element (#5)
|
|
1852
|
-
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
1853
|
-
if (state.selectedEl && !ps(state.selectedEl)) {
|
|
1854
|
-
e.preventDefault();
|
|
1855
|
-
const el = state.selectedEl;
|
|
1856
|
-
// Record removal in history
|
|
1857
|
-
rec(el, { display: 'none' }, { display: el.style.display || '' });
|
|
1858
|
-
el.style.display = 'none';
|
|
1859
|
-
// Deselect
|
|
1860
|
-
el.classList.remove('__ps__', '__ps_multi__');
|
|
1861
|
-
state.selectedEl = null;
|
|
1862
|
-
state.selectedEls = [];
|
|
1863
|
-
hdl.classList.remove('v');
|
|
1864
|
-
toast('🗑 Element hidden (Delete)');
|
|
1865
|
-
}
|
|
1866
|
-
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');
|
|
1867
1849
|
}
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1868
1852
|
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
return;
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
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
|
+
}
|
|
1878
1859
|
|
|
1879
|
-
//
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
});
|
|
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 = [];
|
|
1892
1872
|
hdl.classList.remove('v');
|
|
1893
|
-
|
|
1894
|
-
toast('👁 Preview mode — press P to exit');
|
|
1895
|
-
} else {
|
|
1896
|
-
psRoot.style.display = '';
|
|
1897
|
-
toast('Editor restored');
|
|
1873
|
+
toast('🗑 Element hidden (Delete)');
|
|
1898
1874
|
}
|
|
1875
|
+
return;
|
|
1899
1876
|
}
|
|
1900
1877
|
|
|
1901
|
-
//
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
if (el.id) return '#' + el.id;
|
|
1907
|
-
// Build unique path up the DOM
|
|
1908
|
-
const parts = [];
|
|
1909
|
-
let cur = el;
|
|
1910
|
-
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
1911
|
-
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
1912
|
-
const tag = cur.tagName.toLowerCase();
|
|
1913
|
-
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
1914
|
-
const siblings = cur.parentElement
|
|
1915
|
-
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
1916
|
-
: [];
|
|
1917
|
-
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
1918
|
-
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
1919
|
-
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();
|
|
1920
1883
|
}
|
|
1921
|
-
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');
|
|
1922
1907
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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;
|
|
1926
1929
|
}
|
|
1927
|
-
|
|
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
|
+
}) ();
|