draply-dev 1.5.4 → 1.5.6
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 +238 -110
- package/package.json +27 -27
- package/src/overlay.js +1258 -1097
package/src/overlay.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
dragSX: 0, dragSY: 0, dragOrigL: 0, dragOrigT: 0,
|
|
10
10
|
dragOrigPositions: [], // [{el, origL, origT, prevL, prevT}] for multi-drag
|
|
11
11
|
resizeDir: '', resizeOrigW: 0, resizeOrigH: 0, resizeOrigX: 0, resizeOrigY: 0, resizeOrigL: 0, resizeOrigT: 0,
|
|
12
|
-
isPro: true, // set
|
|
12
|
+
isPro: window.__DRAPLY_PRO__ === true, // set via cli.js injection
|
|
13
13
|
changes: []
|
|
14
14
|
};
|
|
15
15
|
|
|
@@ -201,6 +201,10 @@
|
|
|
201
201
|
background: #1e1e2e;
|
|
202
202
|
margin: 5px 6px;
|
|
203
203
|
}
|
|
204
|
+
.pro-font {
|
|
205
|
+
color: #5cdb4f;
|
|
206
|
+
font-weight: 700;
|
|
207
|
+
}
|
|
204
208
|
|
|
205
209
|
/* ── SUB-PANELS ───────────────────────────────────── */
|
|
206
210
|
.ps-sub {
|
|
@@ -612,31 +616,119 @@
|
|
|
612
616
|
<div class="ps-sec" style="padding:8px 0 4px">Font</div>
|
|
613
617
|
<select class="ps-select" id="__typ_font__" style="width:100%;margin-bottom:6px">
|
|
614
618
|
<option value="">— keep current —</option>
|
|
615
|
-
<optgroup label="Sans-
|
|
619
|
+
<optgroup label="Free Sans-Serif">
|
|
616
620
|
<option value="Inter">Inter</option>
|
|
617
621
|
<option value="Roboto">Roboto</option>
|
|
622
|
+
<option value="Open Sans">Open Sans</option>
|
|
623
|
+
<option value="Lato">Lato</option>
|
|
624
|
+
<option value="Montserrat">Montserrat</option>
|
|
618
625
|
<option value="Poppins">Poppins</option>
|
|
619
|
-
<option value="
|
|
620
|
-
<option value="
|
|
621
|
-
<option value="
|
|
622
|
-
<option value="
|
|
626
|
+
<option value="Raleway">Raleway</option>
|
|
627
|
+
<option value="Nunito">Nunito</option>
|
|
628
|
+
<option value="Ubuntu">Ubuntu</option>
|
|
629
|
+
<option value="Rubik">Rubik</option>
|
|
630
|
+
<option value="Work Sans">Work Sans</option>
|
|
631
|
+
<option value="Quicksand">Quicksand</option>
|
|
632
|
+
<option value="Karla">Karla</option>
|
|
633
|
+
<option value="Josefin Sans">Josefin Sans</option>
|
|
634
|
+
<option value="Asap">Asap</option>
|
|
635
|
+
<option value="Hind">Hind</option>
|
|
636
|
+
<option value="Dosis">Dosis</option>
|
|
637
|
+
<option value="Cabin">Cabin</option>
|
|
638
|
+
<option value="Mukta">Mukta</option>
|
|
639
|
+
<option value="PT Sans">PT Sans</option>
|
|
640
|
+
</optgroup>
|
|
641
|
+
<optgroup label="Pro Sans-Serif">
|
|
642
|
+
<option value="Manrope" class="pro-font">Manrope ✦ PRO</option>
|
|
643
|
+
<option value="DM Sans" class="pro-font">DM Sans ✦ PRO</option>
|
|
644
|
+
<option value="Outfit" class="pro-font">Outfit ✦ PRO</option>
|
|
645
|
+
<option value="Plus Jakarta Sans" class="pro-font">Plus Jakarta Sans ✦ PRO</option>
|
|
646
|
+
<option value="Sora" class="pro-font">Sora ✦ PRO</option>
|
|
647
|
+
<option value="Epilogue" class="pro-font">Epilogue ✦ PRO</option>
|
|
648
|
+
<option value="Lexend" class="pro-font">Lexend ✦ PRO</option>
|
|
649
|
+
<option value="Urbanist" class="pro-font">Urbanist ✦ PRO</option>
|
|
650
|
+
<option value="Archivo" class="pro-font">Archivo ✦ PRO</option>
|
|
651
|
+
<option value="Bricolage Grotesque" class="pro-font">Bricolage Grotesque ✦ PRO</option>
|
|
652
|
+
<option value="Albert Sans" class="pro-font">Albert Sans ✦ PRO</option>
|
|
653
|
+
<option value="Schibsted Grotesk" class="pro-font">Schibsted Grotesk ✦ PRO</option>
|
|
654
|
+
<option value="Figtree" class="pro-font">Figtree ✦ PRO</option>
|
|
655
|
+
<option value="Hanken Grotesk" class="pro-font">Hanken Grotesk ✦ PRO</option>
|
|
656
|
+
<option value="Public Sans" class="pro-font">Public Sans ✦ PRO</option>
|
|
657
|
+
<option value="Be Vietnam Pro" class="pro-font">Be Vietnam Pro ✦ PRO</option>
|
|
658
|
+
<option value="Sen" class="pro-font">Sen ✦ PRO</option>
|
|
659
|
+
<option value="Chivo" class="pro-font">Chivo ✦ PRO</option>
|
|
660
|
+
<option value="Questrial" class="pro-font">Questrial ✦ PRO</option>
|
|
661
|
+
<option value="Jost" class="pro-font">Jost ✦ PRO</option>
|
|
623
662
|
</optgroup>
|
|
624
|
-
<optgroup label="Serif">
|
|
663
|
+
<optgroup label="Free Serif">
|
|
625
664
|
<option value="Playfair Display">Playfair Display</option>
|
|
626
665
|
<option value="Merriweather">Merriweather</option>
|
|
627
666
|
<option value="Lora">Lora</option>
|
|
667
|
+
<option value="PT Serif">PT Serif</option>
|
|
668
|
+
<option value="Noto Serif">Noto Serif</option>
|
|
669
|
+
<option value="Libre Baskerville">Libre Baskerville</option>
|
|
670
|
+
<option value="Arvo">Arvo</option>
|
|
671
|
+
<option value="Roboto Slab">Roboto Slab</option>
|
|
672
|
+
<option value="Bitter">Bitter</option>
|
|
673
|
+
<option value="Crimson Text">Crimson Text</option>
|
|
628
674
|
</optgroup>
|
|
629
|
-
<optgroup label="
|
|
630
|
-
<option value="
|
|
675
|
+
<optgroup label="Pro Serif">
|
|
676
|
+
<option value="EB Garamond" class="pro-font">EB Garamond ✦ PRO</option>
|
|
677
|
+
<option value="Cormorant" class="pro-font">Cormorant ✦ PRO</option>
|
|
678
|
+
<option value="Crimson Pro" class="pro-font">Crimson Pro ✦ PRO</option>
|
|
679
|
+
<option value="Zilla Slab" class="pro-font">Zilla Slab ✦ PRO</option>
|
|
680
|
+
<option value="Spectral" class="pro-font">Spectral ✦ PRO</option>
|
|
681
|
+
<option value="Fraunces" class="pro-font">Fraunces ✦ PRO</option>
|
|
682
|
+
<option value="Literata" class="pro-font">Literata ✦ PRO</option>
|
|
683
|
+
<option value="Petrona" class="pro-font">Petrona ✦ PRO</option>
|
|
684
|
+
<option value="Tinos" class="pro-font">Tinos ✦ PRO</option>
|
|
685
|
+
<option value="Domine" class="pro-font">Domine ✦ PRO</option>
|
|
686
|
+
<option value="Vesper Libre" class="pro-font">Vesper Libre ✦ PRO</option>
|
|
687
|
+
<option value="Prata" class="pro-font">Prata ✦ PRO</option>
|
|
688
|
+
<option value="Alice" class="pro-font">Alice ✦ PRO</option>
|
|
689
|
+
</optgroup>
|
|
690
|
+
<optgroup label="Free Display">
|
|
631
691
|
<option value="Oswald">Oswald</option>
|
|
632
|
-
<option value="
|
|
633
|
-
<option value="
|
|
634
|
-
<option value="
|
|
692
|
+
<option value="Bebas Neue">Bebas Neue</option>
|
|
693
|
+
<option value="Righteous">Righteous</option>
|
|
694
|
+
<option value="Fjalla One">Fjalla One</option>
|
|
695
|
+
<option value="Lobster">Lobster</option>
|
|
696
|
+
<option value="Pacifico">Pacifico</option>
|
|
697
|
+
<option value="Shadows Into Light">Shadows Into Light</option>
|
|
698
|
+
<option value="Dancing Script">Dancing Script</option>
|
|
699
|
+
<option value="Caveat">Caveat</option>
|
|
700
|
+
</optgroup>
|
|
701
|
+
<optgroup label="Pro Display">
|
|
702
|
+
<option value="Syne" class="pro-font">Syne ✦ PRO</option>
|
|
703
|
+
<option value="Space Grotesk" class="pro-font">Space Grotesk ✦ PRO</option>
|
|
704
|
+
<option value="Anton" class="pro-font">Anton ✦ PRO</option>
|
|
705
|
+
<option value="Titan One" class="pro-font">Titan One ✦ PRO</option>
|
|
706
|
+
<option value="Knewave" class="pro-font">Knewave ✦ PRO</option>
|
|
707
|
+
<option value="Bungee" class="pro-font">Bungee ✦ PRO</option>
|
|
708
|
+
<option value="Abril Fatface" class="pro-font">Abril Fatface ✦ PRO</option>
|
|
709
|
+
<option value="Cinzel" class="pro-font">Cinzel ✦ PRO</option>
|
|
710
|
+
<option value="Permanent Marker" class="pro-font">Permanent Marker ✦ PRO</option>
|
|
711
|
+
<option value="Alfa Slab One" class="pro-font">Alfa Slab One ✦ PRO</option>
|
|
712
|
+
<option value="Bangers" class="pro-font">Bangers ✦ PRO</option>
|
|
713
|
+
<option value="Creepster" class="pro-font">Creepster ✦ PRO</option>
|
|
714
|
+
<option value="Monoton" class="pro-font">Monoton ✦ PRO</option>
|
|
715
|
+
<option value="Black Ops One" class="pro-font">Black Ops One ✦ PRO</option>
|
|
716
|
+
<option value="Faster One" class="pro-font">Faster One ✦ PRO</option>
|
|
635
717
|
</optgroup>
|
|
636
|
-
<optgroup label="Mono">
|
|
718
|
+
<optgroup label="Free Mono">
|
|
637
719
|
<option value="Space Mono">Space Mono</option>
|
|
638
|
-
<option value="JetBrains Mono">JetBrains Mono</option>
|
|
639
720
|
<option value="Fira Code">Fira Code</option>
|
|
721
|
+
<option value="Roboto Mono">Roboto Mono</option>
|
|
722
|
+
<option value="Inconsolata">Inconsolata</option>
|
|
723
|
+
<option value="Source Code Pro">Source Code Pro</option>
|
|
724
|
+
</optgroup>
|
|
725
|
+
<optgroup label="Pro Mono">
|
|
726
|
+
<option value="JetBrains Mono" class="pro-font">JetBrains Mono ✦ PRO</option>
|
|
727
|
+
<option value="IBM Plex Mono" class="pro-font">IBM Plex Mono ✦ PRO</option>
|
|
728
|
+
<option value="Ubuntu Mono" class="pro-font">Ubuntu Mono ✦ PRO</option>
|
|
729
|
+
<option value="PT Mono" class="pro-font">PT Mono ✦ PRO</option>
|
|
730
|
+
<option value="Anonymous Pro" class="pro-font">Anonymous Pro ✦ PRO</option>
|
|
731
|
+
<option value="Cousine" class="pro-font">Cousine ✦ PRO</option>
|
|
640
732
|
</optgroup>
|
|
641
733
|
</select>
|
|
642
734
|
|
|
@@ -678,6 +770,7 @@
|
|
|
678
770
|
|
|
679
771
|
<div class="ps-foot">
|
|
680
772
|
<button id="__ps_sv__" disabled>SAVE CHANGES</button>
|
|
773
|
+
<button id="__ps_ai_cfg__" style="width:100%;margin-top:8px;background:none;border:1px solid #2a2a3a;border-radius:6px;padding:6px;color:#5a5a7a;font-size:9px;cursor:pointer;letter-spacing:1px;text-transform:uppercase;transition:color .15s, border-color .15s" onmouseover="this.style.color='#7fff6e'; this.style.borderColor='#7fff6e'" onmouseout="this.style.color='#5a5a7a'; this.style.borderColor='#2a2a3a'">⚙️ AI Provider</button>
|
|
681
774
|
</div>
|
|
682
775
|
</div>
|
|
683
776
|
|
|
@@ -698,16 +791,15 @@
|
|
|
698
791
|
<div id="__ps_tst__"></div>
|
|
699
792
|
|
|
700
793
|
<!-- PAYWALL MODAL -->
|
|
701
|
-
<div id="__ps_paywall__" style="display:none;position:fixed;inset:0;z-index:2147483645;background:rgba(0,0,0,.82);backdrop-filter:blur(8px);align-items:center;justify-content:center;">
|
|
702
|
-
<div style="background
|
|
703
|
-
<div style="position:absolute;top:-
|
|
704
|
-
<div style="font-size:
|
|
705
|
-
<div style="color:#
|
|
706
|
-
<div
|
|
707
|
-
<div style="
|
|
708
|
-
|
|
709
|
-
<
|
|
710
|
-
<button id="__pw_close__" style="background:none;border:1px solid #1e1e3a;color:#44445a;border-radius:8px;padding:9px;font-size:10px;cursor:pointer;letter-spacing:.3px">Maybe later</button>
|
|
794
|
+
<div id="__ps_paywall__" style="display:none;position:fixed;inset:0;z-index:2147483645;background:rgba(0,0,0,.82);backdrop-filter:blur(8px);align-items:center;justify-content:center;font-family:system-ui,-apple-system,sans-serif;">
|
|
795
|
+
<div style="background:#131313;border:1px solid #2a2a3a;border-radius:16px;padding:48px 36px;width:340px;text-align:center;box-shadow:0 24px 80px rgba(0,0,0,.9),0 0 0 1px rgba(92,219,79,.1);position:relative;overflow:hidden;">
|
|
796
|
+
<div style="position:absolute;top:-60px;left:50%;transform:translateX(-50%);width:200px;height:200px;background:radial-gradient(circle,rgba(92,219,79,.1) 0%,transparent 70%);pointer-events:none"></div>
|
|
797
|
+
<div style="color:#fff;font-size:13px;font-weight:800;margin-bottom:8px;letter-spacing:1px;position:relative;text-transform:uppercase;">PRO FEATURE</div>
|
|
798
|
+
<div id="__pw_tool_name__" style="color:#5cdb4f;font-size:24px;font-weight:800;margin-bottom:16px;letter-spacing:.5px;position:relative"></div>
|
|
799
|
+
<div style="color:#a0a0b8;font-size:13px;line-height:1.6;margin-bottom:32px;position:relative">Inspect, Resize, Colors, Typography and Assets are available in <span style="color:#5cdb4f;font-weight:600">Draply Pro</span>.<br><br>Select and Move are always free.</div>
|
|
800
|
+
<div style="position:relative;display:flex;flex-direction:column;gap:12px">
|
|
801
|
+
<a id="__pw_upgrade__" href="http://localhost:3000" target="_blank" style="background:#5cdb4f;color:#131313;border:none;border-radius:10px;padding:14px;font-size:13px;font-weight:800;cursor:pointer;letter-spacing:.6px;text-decoration:none;display:block;">GET DRAPLY PRO →</a>
|
|
802
|
+
<button id="__pw_close__" style="background:none;border:1px solid #2a2a3a;color:#a0a0b8;border-radius:10px;padding:12px;font-size:12px;cursor:pointer;letter-spacing:.3px;">Maybe later</button>
|
|
711
803
|
</div>
|
|
712
804
|
</div>
|
|
713
805
|
</div>
|
|
@@ -771,8 +863,8 @@
|
|
|
771
863
|
|
|
772
864
|
Object.entries(toolBtns).forEach(([t, btn]) => btn.onclick = () => setT(t));
|
|
773
865
|
|
|
774
|
-
// Freemium: only sel
|
|
775
|
-
const FREE_TOOLS = ['sel', 'mov'];
|
|
866
|
+
// Freemium: only sel, mov, typ are free
|
|
867
|
+
const FREE_TOOLS = ['sel', 'mov', 'typ'];
|
|
776
868
|
const TOOL_LABELS = { ins: 'Inspect', rsz: 'Resize', clr: 'Colors', typ: 'Typography', ast: 'Assets' };
|
|
777
869
|
const paywall = document.getElementById('__ps_paywall__');
|
|
778
870
|
const pwToolName = document.getElementById('__pw_tool_name__');
|
|
@@ -828,1172 +920,1241 @@
|
|
|
828
920
|
|
|
829
921
|
// ── HOVER ────────────────────────────────────────────────────────────────
|
|
830
922
|
document.addEventListener('mouseover', e => {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
});
|
|
840
|
-
document.addEventListener('mouseout', e => {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
});
|
|
844
|
-
document.addEventListener('mousemove', e => {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
// ── CLICK SELECT ─────────────────────────────────────────────────────────
|
|
850
|
-
document.addEventListener('click', e => {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
t.classList.remove('__ps_multi__');
|
|
862
|
-
t.classList.remove('__ps__');
|
|
863
|
-
// If we removed the primary, promote the first remaining to primary
|
|
864
|
-
if (t === state.selectedEl) {
|
|
865
|
-
state.selectedEl = state.selectedEls[0] || null;
|
|
866
|
-
if (state.selectedEl) {
|
|
867
|
-
state.selectedEl.classList.remove('__ps_multi__');
|
|
868
|
-
state.selectedEl.classList.add('__ps__');
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
// Add to selection
|
|
873
|
-
state.selectedEls.push(t);
|
|
874
|
-
t.classList.add('__ps_multi__');
|
|
875
|
-
// If no primary yet, make this primary
|
|
876
|
-
if (!state.selectedEl) {
|
|
877
|
-
state.selectedEl = t;
|
|
923
|
+
if (!state.tool || ps(e.target)) return;
|
|
924
|
+
document.querySelectorAll('.__ph__').forEach(el => el.classList.remove('__ph__'));
|
|
925
|
+
e.target.classList.add('__ph__');
|
|
926
|
+
if (state.tool === 'ins') {
|
|
927
|
+
const r = e.target.getBoundingClientRect(), cs = getComputedStyle(e.target);
|
|
928
|
+
tip.textContent = `${Math.round(r.width)}×${Math.round(r.height)} ${cs.position} ${cs.display}`;
|
|
929
|
+
tip.classList.add('v');
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
document.addEventListener('mouseout', e => {
|
|
933
|
+
if (!ps(e.target)) e.target.classList.remove('__ph__');
|
|
934
|
+
if (state.tool === 'ins') tip.classList.remove('v');
|
|
935
|
+
});
|
|
936
|
+
document.addEventListener('mousemove', e => {
|
|
937
|
+
if (state.tool === 'ins') { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
938
|
+
if (state.dragging || state.resizing) { tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px'; }
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
// ── CLICK SELECT ─────────────────────────────────────────────────────────
|
|
942
|
+
document.addEventListener('click', e => {
|
|
943
|
+
if (!state.tool || ps(e.target) || state.tool === 'ins') return;
|
|
944
|
+
e.preventDefault(); e.stopPropagation();
|
|
945
|
+
|
|
946
|
+
if (state.tool === 'mov' && e.ctrlKey) {
|
|
947
|
+
// Ctrl+click: toggle element in multi-select
|
|
948
|
+
const t = e.target;
|
|
949
|
+
const idx = state.selectedEls.indexOf(t);
|
|
950
|
+
if (idx >= 0) {
|
|
951
|
+
// Already in selection — remove it
|
|
952
|
+
state.selectedEls.splice(idx, 1);
|
|
878
953
|
t.classList.remove('__ps_multi__');
|
|
879
|
-
t.classList.
|
|
954
|
+
t.classList.remove('__ps__');
|
|
955
|
+
// If we removed the primary, promote the first remaining to primary
|
|
956
|
+
if (t === state.selectedEl) {
|
|
957
|
+
state.selectedEl = state.selectedEls[0] || null;
|
|
958
|
+
if (state.selectedEl) {
|
|
959
|
+
state.selectedEl.classList.remove('__ps_multi__');
|
|
960
|
+
state.selectedEl.classList.add('__ps__');
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
} else {
|
|
964
|
+
// Add to selection
|
|
965
|
+
state.selectedEls.push(t);
|
|
966
|
+
t.classList.add('__ps_multi__');
|
|
967
|
+
// If no primary yet, make this primary
|
|
968
|
+
if (!state.selectedEl) {
|
|
969
|
+
state.selectedEl = t;
|
|
970
|
+
t.classList.remove('__ps_multi__');
|
|
971
|
+
t.classList.add('__ps__');
|
|
972
|
+
}
|
|
880
973
|
}
|
|
974
|
+
// Show handle on primary if any selected
|
|
975
|
+
if (state.selectedEl) placeHdl(state.selectedEl);
|
|
976
|
+
else hdl.classList.remove('v');
|
|
977
|
+
return;
|
|
881
978
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
979
|
+
|
|
980
|
+
// Normal click (no Ctrl): clear all and select just this
|
|
981
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el => {
|
|
982
|
+
el.classList.remove('__ps__');
|
|
983
|
+
el.classList.remove('__ps_multi__');
|
|
984
|
+
});
|
|
985
|
+
state.selectedEls = [e.target];
|
|
986
|
+
state.selectedEl = e.target;
|
|
987
|
+
e.target.classList.add('__ps__');
|
|
988
|
+
|
|
989
|
+
if (state.tool === 'mov') { dEl = null; placeHdl(e.target); }
|
|
990
|
+
if (state.tool === 'rsz') { rzEl = null; rzDir = null; placeRH(e.target); }
|
|
991
|
+
if (state.tool === 'clr') populateColors(e.target);
|
|
992
|
+
if (state.tool === 'typ') populateTypo(e.target);
|
|
993
|
+
}, true);
|
|
994
|
+
|
|
995
|
+
// ══════════════════════════════════════════
|
|
996
|
+
// MOVE
|
|
997
|
+
// ══════════════════════════════════════════
|
|
998
|
+
function placeHdl(el) {
|
|
999
|
+
const r = el.getBoundingClientRect();
|
|
1000
|
+
const hdlW = 80; // approximate handle width
|
|
1001
|
+
// Center above element, but clamp inside viewport
|
|
1002
|
+
let left = r.left + r.width / 2 - hdlW / 2;
|
|
1003
|
+
let top = r.top - 36;
|
|
1004
|
+
// Clamp horizontally
|
|
1005
|
+
left = Math.max(8, Math.min(left, window.innerWidth - hdlW - 8));
|
|
1006
|
+
// If element is at top, show handle below instead
|
|
1007
|
+
if (top < 8) top = r.bottom + 8;
|
|
1008
|
+
hdl.style.left = left + 'px';
|
|
1009
|
+
hdl.style.top = top + 'px';
|
|
1010
|
+
hdl.classList.add('v');
|
|
886
1011
|
}
|
|
887
1012
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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' };
|
|
937
|
-
});
|
|
1013
|
+
let dEl;
|
|
1014
|
+
hdl.addEventListener('mousedown', e => {
|
|
1015
|
+
if (!state.selectedEl) return;
|
|
1016
|
+
e.preventDefault();
|
|
1017
|
+
dEl = state.selectedEl;
|
|
1018
|
+
state.dragSX = e.clientX; state.dragSY = e.clientY;
|
|
1019
|
+
|
|
1020
|
+
// Snapshot original positions for ALL selected elements
|
|
1021
|
+
const els = state.selectedEls.length > 0 ? state.selectedEls : [state.selectedEl];
|
|
1022
|
+
state.dragOrigPositions = els.map(el => {
|
|
1023
|
+
const cs = getComputedStyle(el);
|
|
1024
|
+
const origL = parseFloat(cs.left) || 0;
|
|
1025
|
+
const origT = parseFloat(cs.top) || 0;
|
|
1026
|
+
if (cs.position === 'static') el.style.position = 'relative';
|
|
1027
|
+
el.style.transition = 'none';
|
|
1028
|
+
return { el, origL, origT, prevL: Math.round(origL) + 'px', prevT: Math.round(origT) + 'px' };
|
|
1029
|
+
});
|
|
938
1030
|
|
|
939
|
-
|
|
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';
|
|
1031
|
+
state.dragging = true;
|
|
947
1032
|
});
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
});
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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 });
|
|
1033
|
+
document.addEventListener('mousemove', e => {
|
|
1034
|
+
if (!state.dragging) return;
|
|
1035
|
+
const dx = e.clientX - state.dragSX, dy = e.clientY - state.dragSY;
|
|
1036
|
+
state.dragOrigPositions.forEach(({ el, origL, origT }) => {
|
|
1037
|
+
el.style.left = (origL + dx) + 'px';
|
|
1038
|
+
el.style.top = (origT + dy) + 'px';
|
|
1039
|
+
});
|
|
1040
|
+
if (dEl) {
|
|
1041
|
+
placeHdl(dEl);
|
|
1042
|
+
const { origL, origT } = state.dragOrigPositions[0];
|
|
1043
|
+
tip.textContent = `x:${Math.round(origL + dx)} y:${Math.round(origT + dy)}`;
|
|
1044
|
+
tip.classList.add('v');
|
|
965
1045
|
}
|
|
966
1046
|
});
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1047
|
+
document.addEventListener('mouseup', () => {
|
|
1048
|
+
if (!state.dragging) return;
|
|
1049
|
+
state.dragging = false; tip.classList.remove('v');
|
|
1050
|
+
// Record each element separately
|
|
1051
|
+
state.dragOrigPositions.forEach(({ el, prevL, prevT }) => {
|
|
1052
|
+
const cs = getComputedStyle(el);
|
|
1053
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1054
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1055
|
+
if (newLeft !== prevL || newTop !== prevT) {
|
|
1056
|
+
rec(el, { left: newLeft, top: newTop }, { left: prevL, top: prevT });
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
state.dragOrigPositions = [];
|
|
1060
|
+
dEl = null;
|
|
1061
|
+
});
|
|
1062
|
+
|
|
1063
|
+
// ══════════════════════════════════════════
|
|
1064
|
+
// RESIZE
|
|
1065
|
+
// ══════════════════════════════════════════
|
|
1066
|
+
function placeRH(el) {
|
|
1067
|
+
const r = el.getBoundingClientRect();
|
|
1068
|
+
// Use viewport coords (fixed position handles need viewport coords, not page)
|
|
1069
|
+
const t = r.top, l = r.left;
|
|
1070
|
+
const hw = r.width, hh = r.height;
|
|
1071
|
+
const hs = 5; // half handle size for centering
|
|
1072
|
+
|
|
1073
|
+
const pos = {
|
|
1074
|
+
nw: [l - hs, t - hs],
|
|
1075
|
+
n: [l + hw / 2 - hs, t - hs],
|
|
1076
|
+
ne: [l + hw - hs, t - hs],
|
|
1077
|
+
w: [l - hs, t + hh / 2 - hs],
|
|
1078
|
+
e: [l + hw - hs, t + hh / 2 - hs],
|
|
1079
|
+
sw: [l - hs, t + hh - hs],
|
|
1080
|
+
s: [l + hw / 2 - hs, t + hh - hs],
|
|
1081
|
+
se: [l + hw - hs, t + hh - hs],
|
|
1082
|
+
};
|
|
1083
|
+
rhDirs.forEach(d => {
|
|
1084
|
+
rhs[d].style.left = pos[d][0] + 'px';
|
|
1085
|
+
rhs[d].style.top = pos[d][1] + 'px';
|
|
1086
|
+
rhs[d].classList.add('v');
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
let rzEl = null, rzDir = null;
|
|
991
1091
|
rhDirs.forEach(d => {
|
|
992
|
-
rhs[d].
|
|
993
|
-
|
|
994
|
-
|
|
1092
|
+
rhs[d].addEventListener('mousedown', e => {
|
|
1093
|
+
if (!state.selectedEl) return;
|
|
1094
|
+
e.preventDefault(); e.stopPropagation();
|
|
1095
|
+
rzEl = state.selectedEl; rzDir = d;
|
|
1096
|
+
const r = rzEl.getBoundingClientRect();
|
|
1097
|
+
const cs = getComputedStyle(rzEl);
|
|
1098
|
+
state.resizeOrigW = r.width; state.resizeOrigH = r.height;
|
|
1099
|
+
state.resizeOrigX = e.clientX; state.resizeOrigY = e.clientY;
|
|
1100
|
+
state.resizeOrigL = parseFloat(cs.left) || 0;
|
|
1101
|
+
state.resizeOrigT = parseFloat(cs.top) || 0;
|
|
1102
|
+
if (cs.position === 'static') rzEl.style.position = 'relative';
|
|
1103
|
+
rzEl.style.transition = 'none';
|
|
1104
|
+
state.resizing = true;
|
|
1105
|
+
});
|
|
995
1106
|
});
|
|
996
|
-
}
|
|
997
1107
|
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1108
|
+
document.addEventListener('mousemove', e => {
|
|
1109
|
+
if (!state.resizing || !rzEl) return;
|
|
1110
|
+
const dx = e.clientX - state.resizeOrigX, dy = e.clientY - state.resizeOrigY;
|
|
1111
|
+
let w = state.resizeOrigW, h = state.resizeOrigH;
|
|
1112
|
+
let l = state.resizeOrigL, t = state.resizeOrigT;
|
|
1113
|
+
|
|
1114
|
+
if (rzDir.includes('e')) w = Math.max(20, w + dx);
|
|
1115
|
+
if (rzDir.includes('s')) h = Math.max(20, h + dy);
|
|
1116
|
+
if (rzDir.includes('w')) { w = Math.max(20, w - dx); l = state.resizeOrigL + (state.resizeOrigW - w); }
|
|
1117
|
+
if (rzDir.includes('n')) { h = Math.max(20, h - dy); t = state.resizeOrigT + (state.resizeOrigH - h); }
|
|
1118
|
+
|
|
1119
|
+
rzEl.style.width = Math.round(w) + 'px';
|
|
1120
|
+
rzEl.style.height = Math.round(h) + 'px';
|
|
1121
|
+
rzEl.style.left = Math.round(l) + 'px';
|
|
1122
|
+
rzEl.style.top = Math.round(t) + 'px';
|
|
1123
|
+
placeRH(rzEl);
|
|
1124
|
+
tip.textContent = `${Math.round(w)}×${Math.round(h)}`;
|
|
1125
|
+
tip.classList.add('v');
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
document.addEventListener('mouseup', () => {
|
|
1129
|
+
if (!state.resizing || !rzEl) return;
|
|
1130
|
+
state.resizing = false; tip.classList.remove('v');
|
|
1005
1131
|
const cs = getComputedStyle(rzEl);
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1132
|
+
// prevProps = original values before resize started
|
|
1133
|
+
const prevProps = {
|
|
1134
|
+
width: Math.round(state.resizeOrigW) + 'px',
|
|
1135
|
+
height: Math.round(state.resizeOrigH) + 'px',
|
|
1136
|
+
left: Math.round(state.resizeOrigL) + 'px',
|
|
1137
|
+
top: Math.round(state.resizeOrigT) + 'px',
|
|
1138
|
+
};
|
|
1139
|
+
const newSize = {
|
|
1140
|
+
width: Math.round(parseFloat(cs.width)) + 'px',
|
|
1141
|
+
height: Math.round(parseFloat(cs.height)) + 'px',
|
|
1142
|
+
left: Math.round(parseFloat(cs.left)) + 'px',
|
|
1143
|
+
top: Math.round(parseFloat(cs.top)) + 'px',
|
|
1144
|
+
};
|
|
1145
|
+
if (newSize.width !== prevProps.width || newSize.height !== prevProps.height || newSize.left !== prevProps.left || newSize.top !== prevProps.top) {
|
|
1146
|
+
rec(rzEl, newSize, prevProps);
|
|
1147
|
+
}
|
|
1148
|
+
rzEl = null; rzDir = null;
|
|
1013
1149
|
});
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
+
|
|
1151
|
+
// ══════════════════════════════════════════
|
|
1152
|
+
// COLORS
|
|
1153
|
+
// ══════════════════════════════════════════
|
|
1154
|
+
const clrElName = document.getElementById('__clr_el_name__');
|
|
1155
|
+
const swBg = document.getElementById('__sw_bg__'), cpBg = document.getElementById('__cp_bg__');
|
|
1156
|
+
const swFg = document.getElementById('__sw_fg__'), cpFg = document.getElementById('__cp_fg__');
|
|
1157
|
+
const swBd = document.getElementById('__sw_bd__'), cpBd = document.getElementById('__cp_bd__');
|
|
1158
|
+
const cpBgTrans = document.getElementById('__cp_bg_trans__');
|
|
1159
|
+
const cpBdTrans = document.getElementById('__cp_bd_trans__');
|
|
1160
|
+
|
|
1161
|
+
function setupSwatch(sw, cp) {
|
|
1162
|
+
sw.onclick = () => cp.click();
|
|
1163
|
+
cp.oninput = () => { sw.style.background = cp.value; };
|
|
1164
|
+
}
|
|
1165
|
+
setupSwatch(swBg, cpBg);
|
|
1166
|
+
setupSwatch(swFg, cpFg);
|
|
1167
|
+
setupSwatch(swBd, cpBd);
|
|
1168
|
+
|
|
1169
|
+
// Transparent checkbox toggles swatch appearance
|
|
1170
|
+
cpBgTrans.onchange = () => {
|
|
1171
|
+
swBg.style.background = cpBgTrans.checked
|
|
1172
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1173
|
+
: cpBg.value;
|
|
1174
|
+
swBg.style.opacity = cpBgTrans.checked ? '0.6' : '1';
|
|
1046
1175
|
};
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1176
|
+
cpBdTrans.onchange = () => {
|
|
1177
|
+
swBd.style.background = cpBdTrans.checked
|
|
1178
|
+
? 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px'
|
|
1179
|
+
: cpBd.value;
|
|
1180
|
+
swBd.style.opacity = cpBdTrans.checked ? '0.6' : '1';
|
|
1052
1181
|
};
|
|
1053
|
-
|
|
1054
|
-
|
|
1182
|
+
|
|
1183
|
+
function isTransparent(color) {
|
|
1184
|
+
return !color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)';
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function populateColors(el) {
|
|
1188
|
+
const cs = getComputedStyle(el);
|
|
1189
|
+
clrElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList][0] : '');
|
|
1190
|
+
let bg = cs.backgroundColor;
|
|
1191
|
+
let fg = cs.color || '#000000';
|
|
1192
|
+
let bd = cs.borderColor || '#cccccc';
|
|
1193
|
+
const bw = cs.borderWidth;
|
|
1194
|
+
|
|
1195
|
+
// #14: Support SVG fill/stroke (#14)
|
|
1196
|
+
const isSvg = el.namespaceURI === 'http://www.w3.org/2000/svg';
|
|
1197
|
+
if (isSvg) {
|
|
1198
|
+
fg = cs.fill;
|
|
1199
|
+
bd = cs.stroke;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Background — detect transparent
|
|
1203
|
+
if (isTransparent(bg)) {
|
|
1204
|
+
cpBgTrans.checked = true;
|
|
1205
|
+
swBg.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1206
|
+
swBg.style.opacity = '0.6';
|
|
1207
|
+
} else {
|
|
1208
|
+
cpBgTrans.checked = false;
|
|
1209
|
+
cpBg.value = rgb2hex(bg);
|
|
1210
|
+
swBg.style.background = cpBg.value;
|
|
1211
|
+
swBg.style.opacity = '1';
|
|
1212
|
+
}
|
|
1213
|
+
cpFg.value = rgb2hex(fg); swFg.style.background = cpFg.value;
|
|
1214
|
+
// Border — detect none/transparent
|
|
1215
|
+
if (isTransparent(bd) || bw === '0px') {
|
|
1216
|
+
cpBdTrans.checked = true;
|
|
1217
|
+
swBd.style.background = 'repeating-conic-gradient(#444 0% 25%, #222 0% 50%) 0 0/10px 10px';
|
|
1218
|
+
swBd.style.opacity = '0.6';
|
|
1219
|
+
} else {
|
|
1220
|
+
cpBdTrans.checked = false;
|
|
1221
|
+
cpBd.value = rgb2hex(bd);
|
|
1222
|
+
swBd.style.background = cpBd.value;
|
|
1223
|
+
swBd.style.opacity = '1';
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// #11: Advanced styles detection
|
|
1227
|
+
const opVal = parseFloat(cs.opacity) || 1;
|
|
1228
|
+
document.getElementById('__st_opacity__').value = opVal;
|
|
1229
|
+
document.getElementById('__st_opacity_val__').textContent = opVal.toFixed(2);
|
|
1230
|
+
document.getElementById('__st_radius__').value = parseInt(cs.borderRadius) || 0;
|
|
1231
|
+
document.getElementById('__st_shadow__').value = cs.boxShadow === 'none' ? 'none' : '0 4px 12px rgba(0,0,0,0.15)'; // simple match
|
|
1055
1232
|
}
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1233
|
+
|
|
1234
|
+
// #11: Live preview for advanced styles
|
|
1235
|
+
document.getElementById('__st_opacity__').oninput = e => {
|
|
1236
|
+
const v = e.target.value;
|
|
1237
|
+
document.getElementById('__st_opacity_val__').textContent = parseFloat(v).toFixed(2);
|
|
1238
|
+
if (state.selectedEl) state.selectedEl.style.opacity = v;
|
|
1239
|
+
};
|
|
1240
|
+
document.getElementById('__st_radius__').oninput = e => {
|
|
1241
|
+
if (state.selectedEl) state.selectedEl.style.borderRadius = e.target.value + 'px';
|
|
1242
|
+
};
|
|
1243
|
+
document.getElementById('__st_shadow__').onchange = e => {
|
|
1244
|
+
if (state.selectedEl) state.selectedEl.style.boxShadow = e.target.value === 'none' ? '' : e.target.value;
|
|
1245
|
+
};
|
|
1246
|
+
|
|
1247
|
+
document.getElementById('__clr_apply__').onclick = () => {
|
|
1248
|
+
if (!state.selectedEl) { toast('⚠ Select an element first'); return; }
|
|
1249
|
+
const el = state.selectedEl;
|
|
1250
|
+
const bgVal = cpBgTrans.checked ? 'transparent' : cpBg.value;
|
|
1251
|
+
const bdVal = cpBdTrans.checked ? 'none' : cpBd.value;
|
|
1252
|
+
const opVal = document.getElementById('__st_opacity__').value;
|
|
1253
|
+
const radVal = document.getElementById('__st_radius__').value + 'px';
|
|
1254
|
+
const shVal = document.getElementById('__st_shadow__').value;
|
|
1255
|
+
|
|
1256
|
+
const prevProps = {
|
|
1257
|
+
'background-color': el.style.backgroundColor || '',
|
|
1258
|
+
'color': el.style.color || '',
|
|
1259
|
+
'border': el.style.border || '',
|
|
1260
|
+
'opacity': el.style.opacity || '',
|
|
1261
|
+
'border-radius': el.style.borderRadius || '',
|
|
1262
|
+
'box-shadow': el.style.boxShadow || ''
|
|
1263
|
+
};
|
|
1264
|
+
|
|
1265
|
+
if (el.namespaceURI === 'http://www.w3.org/2000/svg') {
|
|
1266
|
+
el.style.fill = cpFg.value;
|
|
1267
|
+
el.style.stroke = cpBdTrans.checked ? 'none' : bdVal;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
el.style.backgroundColor = bgVal;
|
|
1271
|
+
el.style.color = cpFg.value;
|
|
1272
|
+
el.style.opacity = opVal;
|
|
1273
|
+
el.style.borderRadius = radVal;
|
|
1274
|
+
el.style.boxShadow = shVal === 'none' ? '' : shVal;
|
|
1275
|
+
|
|
1276
|
+
if (cpBdTrans.checked) {
|
|
1277
|
+
el.style.border = 'none';
|
|
1278
|
+
} else {
|
|
1279
|
+
el.style.borderColor = bdVal;
|
|
1280
|
+
if (getComputedStyle(el).borderWidth === '0px') el.style.borderWidth = '1px';
|
|
1281
|
+
if (getComputedStyle(el).borderStyle === 'none') el.style.borderStyle = 'solid';
|
|
1282
|
+
}
|
|
1283
|
+
rec(el, {
|
|
1284
|
+
'background-color': bgVal, color: cpFg.value,
|
|
1285
|
+
'border': cpBdTrans.checked ? 'none' : `1px solid ${bdVal}`,
|
|
1286
|
+
'opacity': opVal, 'border-radius': radVal, 'box-shadow': shVal === 'none' ? '' : shVal
|
|
1287
|
+
}, prevProps);
|
|
1288
|
+
toast('🎨 Styles applied!');
|
|
1289
|
+
};
|
|
1290
|
+
|
|
1291
|
+
function rgb2hex(rgb) {
|
|
1292
|
+
if (rgb.startsWith('#')) return rgb;
|
|
1293
|
+
const m = rgb.match(/\d+/g);
|
|
1294
|
+
if (!m || m.length < 3) return '#000000';
|
|
1295
|
+
return '#' + m.slice(0, 3).map(x => parseInt(x).toString(16).padStart(2, '0')).join('');
|
|
1108
1296
|
}
|
|
1109
1297
|
|
|
1110
|
-
//
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1298
|
+
// ══════════════════════════════════════════
|
|
1299
|
+
// TYPOGRAPHY
|
|
1300
|
+
// ══════════════════════════════════════════
|
|
1301
|
+
const typElName = document.getElementById('__typ_el_name__');
|
|
1302
|
+
const typSz = document.getElementById('__typ_sz__');
|
|
1303
|
+
const typLh = document.getElementById('__typ_lh__');
|
|
1304
|
+
const typLs = document.getElementById('__typ_ls__');
|
|
1305
|
+
const typFont = document.getElementById('__typ_font__');
|
|
1306
|
+
const typBold = document.getElementById('__typ_bold__');
|
|
1307
|
+
const typItalic = document.getElementById('__typ_italic__');
|
|
1308
|
+
const typUnder = document.getElementById('__typ_under__');
|
|
1309
|
+
const typStrike = document.getElementById('__typ_strike__');
|
|
1310
|
+
const typUpper = document.getElementById('__typ_upper__');
|
|
1311
|
+
const typLower = document.getElementById('__typ_lower__');
|
|
1312
|
+
|
|
1313
|
+
// Track toggle states
|
|
1314
|
+
const typState = { bold: false, italic: false, under: false, strike: false, upper: false, lower: false };
|
|
1315
|
+
|
|
1316
|
+
function setStyleBtn(btn, key, val) {
|
|
1317
|
+
typState[key] = val;
|
|
1318
|
+
btn.classList.toggle('on', val);
|
|
1120
1319
|
}
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1320
|
+
|
|
1321
|
+
typBold.onclick = () => setStyleBtn(typBold, 'bold', !typState.bold);
|
|
1322
|
+
typItalic.onclick = () => setStyleBtn(typItalic, 'italic', !typState.italic);
|
|
1323
|
+
typUnder.onclick = () => setStyleBtn(typUnder, 'under', !typState.under);
|
|
1324
|
+
typStrike.onclick = () => setStyleBtn(typStrike, 'strike', !typState.strike);
|
|
1325
|
+
typUpper.onclick = () => { setStyleBtn(typUpper, 'upper', !typState.upper); if (typState.upper) setStyleBtn(typLower, 'lower', false); };
|
|
1326
|
+
typLower.onclick = () => { setStyleBtn(typLower, 'lower', !typState.lower); if (typState.lower) setStyleBtn(typUpper, 'upper', false); };
|
|
1327
|
+
|
|
1328
|
+
// Loaded Google Fonts cache
|
|
1329
|
+
const loadedFonts = new Set();
|
|
1330
|
+
function loadGoogleFont(name) {
|
|
1331
|
+
if (!name || loadedFonts.has(name)) return;
|
|
1332
|
+
loadedFonts.add(name);
|
|
1333
|
+
const link = document.createElement('link');
|
|
1334
|
+
link.rel = 'stylesheet';
|
|
1335
|
+
link.href = `https://fonts.googleapis.com/css2?family=${encodeURIComponent(name)}:wght@400;700&display=swap`;
|
|
1336
|
+
document.head.appendChild(link);
|
|
1132
1337
|
}
|
|
1133
1338
|
|
|
1134
|
-
//
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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 || ''
|
|
1339
|
+
// Live preview when font changes
|
|
1340
|
+
typFont.onchange = () => {
|
|
1341
|
+
const selectedOpt = typFont.options[typFont.selectedIndex];
|
|
1342
|
+
if (selectedOpt && selectedOpt.classList.contains('pro-font') && !state.isPro) {
|
|
1343
|
+
showPaywall('Premium Fonts');
|
|
1344
|
+
typFont.value = ''; // Revert to empty or you could store the previous value
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (typFont.value) {
|
|
1348
|
+
loadGoogleFont(typFont.value);
|
|
1349
|
+
if (state.selectedEl) previewTypo(state.selectedEl);
|
|
1350
|
+
}
|
|
1171
1351
|
};
|
|
1172
1352
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
el.
|
|
1353
|
+
function populateTypo(el) {
|
|
1354
|
+
// Make text directly editable
|
|
1355
|
+
if (!el.isContentEditable) {
|
|
1356
|
+
el.contentEditable = 'true';
|
|
1357
|
+
el.dataset.origText = el.innerHTML || '';
|
|
1358
|
+
el.focus();
|
|
1359
|
+
|
|
1360
|
+
const finishEdit = () => {
|
|
1361
|
+
el.contentEditable = 'false';
|
|
1362
|
+
el.removeEventListener('blur', finishEdit);
|
|
1363
|
+
if (el.innerHTML !== el.dataset.origText) {
|
|
1364
|
+
rec(el, { innerHTML: el.innerHTML }, { innerHTML: el.dataset.origText });
|
|
1365
|
+
toast('Text updated!');
|
|
1366
|
+
}
|
|
1367
|
+
};
|
|
1368
|
+
el.addEventListener('blur', finishEdit);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const cs = getComputedStyle(el);
|
|
1372
|
+
typElName.textContent = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + [...el.classList].filter(c => !c.startsWith('__'))[0] : '');
|
|
1373
|
+
typSz.value = Math.round(parseFloat(cs.fontSize)) || 16;
|
|
1374
|
+
typLh.value = parseFloat(cs.lineHeight) ? (parseFloat(cs.lineHeight) / parseFloat(cs.fontSize)).toFixed(1) : '1.5';
|
|
1375
|
+
typLs.value = parseFloat(cs.letterSpacing) || 0;
|
|
1376
|
+
typFont.value = '';
|
|
1377
|
+
|
|
1378
|
+
// Detect current styles
|
|
1379
|
+
setStyleBtn(typBold, 'bold', parseInt(cs.fontWeight) >= 700);
|
|
1380
|
+
setStyleBtn(typItalic, 'italic', cs.fontStyle === 'italic');
|
|
1381
|
+
setStyleBtn(typUnder, 'under', cs.textDecoration.includes('underline'));
|
|
1382
|
+
setStyleBtn(typStrike, 'strike', cs.textDecoration.includes('line-through'));
|
|
1383
|
+
setStyleBtn(typUpper, 'upper', cs.textTransform === 'uppercase');
|
|
1384
|
+
setStyleBtn(typLower, 'lower', cs.textTransform === 'lowercase');
|
|
1385
|
+
|
|
1386
|
+
// Live preview on input change
|
|
1387
|
+
[typSz, typLh, typLs].forEach(inp => { inp.oninput = () => previewTypo(el); });
|
|
1176
1388
|
}
|
|
1177
1389
|
|
|
1178
|
-
el
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
el.style.
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1390
|
+
function previewTypo(el) {
|
|
1391
|
+
el.style.fontSize = typSz.value + 'px';
|
|
1392
|
+
el.style.lineHeight = typLh.value;
|
|
1393
|
+
el.style.letterSpacing = typLs.value + 'px';
|
|
1394
|
+
el.style.setProperty('font-weight', typState.bold ? '700' : '400', 'important');
|
|
1395
|
+
el.style.setProperty('font-style', typState.italic ? 'italic' : 'normal', 'important');
|
|
1396
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1397
|
+
el.style.setProperty('text-decoration', deco, 'important');
|
|
1398
|
+
el.style.textTransform = typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none';
|
|
1399
|
+
if (typFont.value) {
|
|
1400
|
+
loadGoogleFont(typFont.value);
|
|
1401
|
+
el.style.fontFamily = `'${typFont.value}', sans-serif`;
|
|
1402
|
+
}
|
|
1190
1403
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
const
|
|
1211
|
-
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
//
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1404
|
+
|
|
1405
|
+
document.getElementById('__typ_apply__').onclick = () => {
|
|
1406
|
+
if (!state.selectedEl) { toast('⚠ Select a text element first'); return; }
|
|
1407
|
+
const el = state.selectedEl;
|
|
1408
|
+
const deco = [typState.under && 'underline', typState.strike && 'line-through'].filter(Boolean).join(' ') || 'none';
|
|
1409
|
+
const props = {
|
|
1410
|
+
'font-size': typSz.value + 'px',
|
|
1411
|
+
'font-weight': typState.bold ? '700' : '400',
|
|
1412
|
+
'font-style': typState.italic ? 'italic' : 'normal',
|
|
1413
|
+
'text-decoration': deco,
|
|
1414
|
+
'text-transform': typState.upper ? 'uppercase' : typState.lower ? 'lowercase' : 'none',
|
|
1415
|
+
'line-height': typLh.value,
|
|
1416
|
+
'letter-spacing': typLs.value + 'px',
|
|
1417
|
+
};
|
|
1418
|
+
if (typFont.value) {
|
|
1419
|
+
props['font-family'] = `'${typFont.value}', sans-serif`;
|
|
1420
|
+
props['googleFont'] = typFont.value;
|
|
1421
|
+
}
|
|
1422
|
+
// Snapshot BEFORE previewTypo applies styles
|
|
1423
|
+
const prevProps = {};
|
|
1424
|
+
Object.keys(props).forEach(p => {
|
|
1425
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1426
|
+
prevProps[p] = el.style[camel] || '';
|
|
1427
|
+
});
|
|
1428
|
+
previewTypo(el);
|
|
1429
|
+
rec(el, props, prevProps);
|
|
1430
|
+
toast('📝 Typography applied!');
|
|
1431
|
+
};
|
|
1432
|
+
|
|
1433
|
+
// ══════════════════════════════════════════
|
|
1434
|
+
// ASSETS
|
|
1435
|
+
// ══════════════════════════════════════════
|
|
1436
|
+
const astDrop = document.getElementById('__ast_drop__');
|
|
1437
|
+
const astFile = document.getElementById('__ast_file__');
|
|
1438
|
+
const astList = document.getElementById('__ast_list__');
|
|
1439
|
+
const astHint = document.getElementById('__ast_hint__');
|
|
1440
|
+
const astStore = []; // { id, name, src, url }
|
|
1441
|
+
let pendingAsset = null; // asset waiting to be placed
|
|
1442
|
+
let placingEl = null; // the ghost element following cursor
|
|
1443
|
+
let astDragEl = null; // placed asset being moved
|
|
1444
|
+
let astDragSX, astDragSY, astDragOL, astDragOT;
|
|
1445
|
+
|
|
1446
|
+
// Upload via click or drop
|
|
1447
|
+
astDrop.onclick = () => astFile.click();
|
|
1448
|
+
astFile.onchange = e => loadAssets(e.target.files);
|
|
1449
|
+
astDrop.ondragover = e => { e.preventDefault(); astDrop.style.borderColor = '#7fff6e'; };
|
|
1450
|
+
astDrop.ondragleave = () => { astDrop.style.borderColor = ''; };
|
|
1451
|
+
astDrop.ondrop = e => {
|
|
1452
|
+
e.preventDefault(); astDrop.style.borderColor = '';
|
|
1453
|
+
loadAssets(e.dataTransfer.files);
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
function loadAssets(files) {
|
|
1457
|
+
[...files].forEach(file => {
|
|
1458
|
+
if (!file.type.startsWith('image/')) return;
|
|
1459
|
+
const reader = new FileReader();
|
|
1460
|
+
reader.onload = async ev => {
|
|
1461
|
+
try {
|
|
1462
|
+
const res = await fetch('/draply-upload', {
|
|
1463
|
+
method: 'POST',
|
|
1464
|
+
body: JSON.stringify({ name: file.name, base64: ev.target.result })
|
|
1465
|
+
});
|
|
1466
|
+
const data = await res.json();
|
|
1467
|
+
if (data.ok) {
|
|
1468
|
+
const asset = { id: Date.now() + Math.random(), name: file.name, src: data.url };
|
|
1469
|
+
astStore.push(asset);
|
|
1470
|
+
addThumb(asset);
|
|
1471
|
+
astHint.style.display = 'block';
|
|
1472
|
+
toast('🖼️ ' + file.name + ' loaded');
|
|
1473
|
+
} else {
|
|
1474
|
+
toast('⚠ Upload failed');
|
|
1475
|
+
}
|
|
1476
|
+
} catch (e) {
|
|
1477
|
+
toast('⚠ Upload failed');
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
reader.readAsDataURL(file);
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function addThumb(asset) {
|
|
1485
|
+
const img = document.createElement('img');
|
|
1486
|
+
img.className = 'ps-ast-thumb';
|
|
1487
|
+
img.src = asset.src;
|
|
1488
|
+
img.title = asset.name;
|
|
1489
|
+
img.dataset.assetId = asset.id;
|
|
1490
|
+
img.onclick = () => {
|
|
1491
|
+
// Deselect all thumbs
|
|
1492
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1493
|
+
img.classList.add('active');
|
|
1494
|
+
startPlacing(asset);
|
|
1266
1495
|
};
|
|
1267
|
-
|
|
1496
|
+
astList.appendChild(img);
|
|
1268
1497
|
}
|
|
1269
1498
|
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
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`;
|
|
1499
|
+
// Start placing: attach ghost image to cursor
|
|
1500
|
+
function startPlacing(asset) {
|
|
1501
|
+
cancelPlacing();
|
|
1502
|
+
pendingAsset = asset;
|
|
1503
|
+
|
|
1504
|
+
// Create ghost
|
|
1505
|
+
placingEl = document.createElement('div');
|
|
1506
|
+
placingEl.className = 'ps-asset-placed placing';
|
|
1507
|
+
placingEl.style.cssText = 'width:120px;height:120px;pointer-events:none;opacity:.75;';
|
|
1508
|
+
placingEl.innerHTML = `<img src="${asset.src}" draggable="false">`;
|
|
1509
|
+
document.body.appendChild(placingEl);
|
|
1510
|
+
|
|
1511
|
+
toast('Click anywhere on page to place');
|
|
1512
|
+
document.addEventListener('mousemove', onPlacingMove);
|
|
1513
|
+
document.addEventListener('click', onPlacingClick, true);
|
|
1514
|
+
document.addEventListener('keydown', onPlacingCancel);
|
|
1301
1515
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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;
|
|
1516
|
+
|
|
1517
|
+
function onPlacingMove(e) {
|
|
1518
|
+
if (!placingEl) return;
|
|
1519
|
+
placingEl.style.left = (e.clientX - 60) + 'px';
|
|
1520
|
+
placingEl.style.top = (e.clientY - 60) + 'px';
|
|
1320
1521
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
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');
|
|
1522
|
+
|
|
1523
|
+
function onPlacingClick(e) {
|
|
1524
|
+
if (ps(e.target)) return; // ignore clicks inside our panel
|
|
1525
|
+
e.preventDefault(); e.stopPropagation();
|
|
1526
|
+
if (!pendingAsset || !placingEl) return;
|
|
1527
|
+
|
|
1528
|
+
if (e.shiftKey || e.altKey) {
|
|
1529
|
+
// #6: Better insertion targeting
|
|
1530
|
+
const target = e.target;
|
|
1531
|
+
if (target.tagName.toLowerCase() === 'img') {
|
|
1532
|
+
const prevSrc = target.getAttribute('src');
|
|
1533
|
+
target.src = pendingAsset.src;
|
|
1534
|
+
rec(target, { src: pendingAsset.src }, { src: prevSrc || '' });
|
|
1535
|
+
toast('🖼️ Image replaced!');
|
|
1536
|
+
} else {
|
|
1537
|
+
// Option to insert INSIDE clicked container if Alt is held
|
|
1538
|
+
if (e.altKey) {
|
|
1539
|
+
placeAsset(pendingAsset, 0, 0, 120, 120, target);
|
|
1540
|
+
toast('✦ Placed inside ' + target.tagName.toLowerCase());
|
|
1372
1541
|
} else {
|
|
1373
|
-
|
|
1542
|
+
const cs = getComputedStyle(target);
|
|
1543
|
+
const prevBg = cs.backgroundImage;
|
|
1544
|
+
target.style.backgroundImage = `url('${pendingAsset.src}')`;
|
|
1545
|
+
target.style.backgroundSize = 'cover';
|
|
1546
|
+
rec(target, { backgroundImage: `url('${pendingAsset.src}')`, backgroundSize: 'cover' }, { backgroundImage: prevBg });
|
|
1547
|
+
toast('🖼️ Background set!');
|
|
1374
1548
|
}
|
|
1375
|
-
} catch (e) {
|
|
1376
|
-
toast('⚠ Upload failed');
|
|
1377
1549
|
}
|
|
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!');
|
|
1435
1550
|
} else {
|
|
1436
|
-
//
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
-
}
|
|
1551
|
+
// PLACE mode (standard)
|
|
1552
|
+
const x = e.clientX - 60;
|
|
1553
|
+
const y = e.clientY - 60;
|
|
1554
|
+
placeAsset(pendingAsset, x, y, 120, 120);
|
|
1448
1555
|
}
|
|
1449
|
-
|
|
1450
|
-
// PLACE mode (standard)
|
|
1451
|
-
const x = e.clientX - 60;
|
|
1452
|
-
const y = e.clientY - 60;
|
|
1453
|
-
placeAsset(pendingAsset, x, y, 120, 120);
|
|
1556
|
+
cancelPlacing();
|
|
1454
1557
|
}
|
|
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
|
-
});
|
|
1490
1558
|
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1559
|
+
function onPlacingCancel(e) {
|
|
1560
|
+
if (e.key === 'Escape') cancelPlacing();
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
function cancelPlacing() {
|
|
1564
|
+
if (placingEl) { placingEl.remove(); placingEl = null; }
|
|
1565
|
+
pendingAsset = null;
|
|
1566
|
+
document.querySelectorAll('.ps-ast-thumb').forEach(t => t.classList.remove('active'));
|
|
1567
|
+
document.removeEventListener('mousemove', onPlacingMove);
|
|
1568
|
+
document.removeEventListener('click', onPlacingClick, true);
|
|
1569
|
+
document.removeEventListener('keydown', onPlacingCancel);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
// #6: Place asset permanently on page (optionally inside a parent)
|
|
1573
|
+
function placeAsset(asset, x, y, w, h, parent = document.body) {
|
|
1574
|
+
const wrap = document.createElement('div');
|
|
1575
|
+
wrap.className = 'ps-asset-placed';
|
|
1576
|
+
const uid = 'ps-asset-' + Date.now();
|
|
1577
|
+
wrap.id = uid;
|
|
1578
|
+
|
|
1579
|
+
// If we have a specific parent, use relative positioning if needed
|
|
1580
|
+
const posType = parent === document.body ? 'fixed' : 'absolute';
|
|
1581
|
+
wrap.style.cssText = `position:${posType};left:${Math.round(x)}px;top:${Math.round(y)}px;width:${w}px;height:${h}px;z-index:1;`;
|
|
1582
|
+
wrap.innerHTML = `<img src="${asset.src}" draggable="false" alt="${asset.name}">`;
|
|
1583
|
+
parent.appendChild(wrap);
|
|
1584
|
+
|
|
1585
|
+
|
|
1586
|
+
// Click to select this placed asset (for z-index control)
|
|
1587
|
+
wrap.addEventListener('click', e => {
|
|
1588
|
+
if (ps(e.target)) return;
|
|
1589
|
+
selectPlaced(wrap);
|
|
1590
|
+
});
|
|
1591
|
+
|
|
1592
|
+
// Make placed asset draggable
|
|
1593
|
+
wrap.addEventListener('mousedown', e => {
|
|
1594
|
+
if (ps(e.target)) return;
|
|
1595
|
+
e.preventDefault();
|
|
1596
|
+
selectPlaced(wrap);
|
|
1597
|
+
astDragEl = wrap;
|
|
1598
|
+
astDragSX = e.clientX; astDragSY = e.clientY;
|
|
1599
|
+
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1600
|
+
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1601
|
+
wrap.style.cursor = 'grabbing';
|
|
1602
|
+
state.dragging = true;
|
|
1603
|
+
});
|
|
1604
|
+
|
|
1605
|
+
rec(wrap, {
|
|
1606
|
+
src: asset.src,
|
|
1607
|
+
left: Math.round(x) + 'px',
|
|
1608
|
+
top: Math.round(y) + 'px',
|
|
1609
|
+
width: w + 'px',
|
|
1610
|
+
height: h + 'px',
|
|
1611
|
+
'z-index': '1',
|
|
1612
|
+
}, null, true); // true = isCreate
|
|
1613
|
+
|
|
1614
|
+
toast('✦ Placed — drag to reposition');
|
|
1495
1615
|
selectPlaced(wrap);
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
astDragOL = parseFloat(wrap.style.left) || 0;
|
|
1499
|
-
astDragOT = parseFloat(wrap.style.top) || 0;
|
|
1500
|
-
wrap.style.cursor = 'grabbing';
|
|
1501
|
-
state.dragging = true;
|
|
1502
|
-
});
|
|
1616
|
+
return wrap;
|
|
1617
|
+
}
|
|
1503
1618
|
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
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;
|
|
1619
|
+
// Track selected placed asset for z-index controls
|
|
1620
|
+
let selectedPlaced = null;
|
|
1621
|
+
const zCtrl = document.getElementById('__ast_zctrl__');
|
|
1622
|
+
const zVal = document.getElementById('__z_val__');
|
|
1623
|
+
const zFront = document.getElementById('__z_front__');
|
|
1624
|
+
const zBack = document.getElementById('__z_back__');
|
|
1625
|
+
const zSet = document.getElementById('__z_set__');
|
|
1626
|
+
|
|
1627
|
+
function selectPlaced(wrap) {
|
|
1628
|
+
// Deselect previous
|
|
1629
|
+
if (selectedPlaced) selectedPlaced.style.outline = '';
|
|
1630
|
+
selectedPlaced = wrap;
|
|
1631
|
+
wrap.style.outline = '2px solid #7fff6e';
|
|
1632
|
+
zVal.value = parseInt(wrap.style.zIndex) || 1;
|
|
1633
|
+
zCtrl.style.display = 'block';
|
|
1571
1634
|
}
|
|
1572
1635
|
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1636
|
+
zFront.onclick = () => {
|
|
1637
|
+
if (!selectedPlaced) return;
|
|
1638
|
+
// Find max z-index of all placed assets
|
|
1639
|
+
const max = Math.max(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1640
|
+
const nz = max + 1;
|
|
1641
|
+
selectedPlaced.style.zIndex = nz;
|
|
1642
|
+
zVal.value = nz;
|
|
1643
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1644
|
+
toast('▲ Moved to front (z:' + nz + ')');
|
|
1645
|
+
};
|
|
1646
|
+
|
|
1647
|
+
zBack.onclick = () => {
|
|
1648
|
+
if (!selectedPlaced) return;
|
|
1649
|
+
const min = Math.min(0, ...[...document.querySelectorAll('.ps-asset-placed')].map(el => parseInt(el.style.zIndex) || 0));
|
|
1650
|
+
const nz = min - 1;
|
|
1651
|
+
selectedPlaced.style.zIndex = nz;
|
|
1652
|
+
zVal.value = nz;
|
|
1653
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1654
|
+
toast('▼ Moved to back (z:' + nz + ')');
|
|
1655
|
+
};
|
|
1656
|
+
|
|
1657
|
+
// #12: LAYERS PANEL LOGIC
|
|
1658
|
+
function updateLayers() {
|
|
1659
|
+
const list = document.getElementById('__lay_list__');
|
|
1660
|
+
list.innerHTML = '';
|
|
1661
|
+
|
|
1662
|
+
// Find interesting elements (headers, sections, divs with classes, images)
|
|
1663
|
+
const items = document.querySelectorAll('body > *:not(#__ps__), section *, header *, .container *');
|
|
1664
|
+
const filtered = [...items].filter(el => {
|
|
1665
|
+
if (ps(el)) return false;
|
|
1666
|
+
return el.tagName !== 'SCRIPT' && el.tagName !== 'STYLE' && (el.children.length === 0 || el.id || el.className);
|
|
1667
|
+
}).slice(0, 50); // limit for performance
|
|
1668
|
+
|
|
1669
|
+
if (!filtered.length) {
|
|
1670
|
+
list.innerHTML = '<div style="color:#444;font-size:10px;text-align:center;padding:10px">No elements found</div>';
|
|
1671
|
+
return;
|
|
1580
1672
|
}
|
|
1581
1673
|
|
|
1582
|
-
|
|
1583
|
-
|
|
1674
|
+
filtered.forEach(el => {
|
|
1675
|
+
const row = document.createElement('div');
|
|
1676
|
+
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';
|
|
1677
|
+
if (state.selectedEl === el) {
|
|
1678
|
+
row.style.borderColor = '#7fff6e';
|
|
1679
|
+
row.style.background = 'rgba(127,255,110,0.1)';
|
|
1680
|
+
row.style.color = '#7fff6e';
|
|
1681
|
+
}
|
|
1584
1682
|
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
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
|
-
const redoHistory = [];
|
|
1644
|
-
|
|
1645
|
-
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1646
|
-
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1647
|
-
const key = el.dataset.pixelshiftId || selector;
|
|
1648
|
-
|
|
1649
|
-
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1650
|
-
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1651
|
-
const prevProps = prevPropsOverride || {};
|
|
1652
|
-
if (!prevPropsOverride) {
|
|
1653
|
-
Object.keys(props).forEach(p => {
|
|
1654
|
-
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1655
|
-
prevProps[p] = el.style[camel] || '';
|
|
1683
|
+
const icon = el.tagName === 'IMG' ? '🖼️' : el.tagName.match(/H[1-6]/) ? 'Tt' : '📦';
|
|
1684
|
+
const name = el.tagName.toLowerCase() + (el.id ? '#' + el.id : el.className ? '.' + el.className.split(' ')[0] : '');
|
|
1685
|
+
|
|
1686
|
+
row.innerHTML = `<span>${icon}</span> <span style="flex:1;overflow:hidden;text-overflow:ellipsis">${name}</span>`;
|
|
1687
|
+
row.onclick = () => {
|
|
1688
|
+
// Select element (inline logic — no separate select() function)
|
|
1689
|
+
document.querySelectorAll('.__ps__, .__ps_multi__').forEach(el2 => {
|
|
1690
|
+
el2.classList.remove('__ps__', '__ps_multi__');
|
|
1691
|
+
});
|
|
1692
|
+
state.selectedEls = [el];
|
|
1693
|
+
state.selectedEl = el;
|
|
1694
|
+
el.classList.add('__ps__');
|
|
1695
|
+
if (state.tool === 'mov') placeHdl(el);
|
|
1696
|
+
if (state.tool === 'rsz') placeRH(el);
|
|
1697
|
+
if (state.tool === 'clr') populateColors(el);
|
|
1698
|
+
if (state.tool === 'typ') populateTypo(el);
|
|
1699
|
+
updateLayers();
|
|
1700
|
+
};
|
|
1701
|
+
list.appendChild(row);
|
|
1656
1702
|
});
|
|
1657
1703
|
}
|
|
1658
1704
|
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1705
|
+
zSet.onclick = () => {
|
|
1706
|
+
if (!selectedPlaced) return;
|
|
1707
|
+
const nz = parseInt(zVal.value) || 0;
|
|
1708
|
+
selectedPlaced.style.zIndex = nz;
|
|
1709
|
+
rec(selectedPlaced, { 'z-index': String(nz) });
|
|
1710
|
+
toast('z-index set to ' + nz);
|
|
1711
|
+
};
|
|
1712
|
+
|
|
1713
|
+
zVal.onkeydown = e => { if (e.key === 'Enter') zSet.click(); };
|
|
1714
|
+
|
|
1715
|
+
// Drag placed assets
|
|
1716
|
+
document.addEventListener('mousemove', e => {
|
|
1717
|
+
if (!astDragEl || !state.dragging) return;
|
|
1718
|
+
const dx = e.clientX - astDragSX, dy = e.clientY - astDragSY;
|
|
1719
|
+
astDragEl.style.left = (astDragOL + dx) + 'px';
|
|
1720
|
+
astDragEl.style.top = (astDragOT + dy) + 'px';
|
|
1721
|
+
tip.textContent = `x:${Math.round(astDragOL + dx)} y:${Math.round(astDragOT + dy)}`;
|
|
1722
|
+
tip.style.left = (e.clientX + 14) + 'px'; tip.style.top = (e.clientY - 28) + 'px';
|
|
1723
|
+
tip.classList.add('v');
|
|
1724
|
+
});
|
|
1725
|
+
|
|
1726
|
+
document.addEventListener('mouseup', () => {
|
|
1727
|
+
if (!astDragEl) return;
|
|
1728
|
+
tip.classList.remove('v');
|
|
1729
|
+
const cs = getComputedStyle(astDragEl);
|
|
1730
|
+
const newLeft = Math.round(parseFloat(cs.left)) + 'px';
|
|
1731
|
+
const newTop = Math.round(parseFloat(cs.top)) + 'px';
|
|
1732
|
+
if (newLeft !== (Math.round(astDragOL) + 'px') || newTop !== (Math.round(astDragOT) + 'px')) {
|
|
1733
|
+
rec(astDragEl, { left: newLeft, top: newTop }, { left: Math.round(astDragOL) + 'px', top: Math.round(astDragOT) + 'px' });
|
|
1734
|
+
}
|
|
1735
|
+
astDragEl.style.cursor = 'grab';
|
|
1736
|
+
astDragEl = null;
|
|
1737
|
+
});
|
|
1738
|
+
|
|
1739
|
+
// ══════════════════════════════════════════
|
|
1740
|
+
// RECORD + SAVE
|
|
1741
|
+
// ══════════════════════════════════════════
|
|
1742
|
+
// Full history — every individual action
|
|
1743
|
+
const history = [];
|
|
1744
|
+
const redoHistory = [];
|
|
1745
|
+
|
|
1746
|
+
function rec(el, props, prevPropsOverride, isCreate = false) {
|
|
1747
|
+
const selector = el.dataset.pixelshiftId ? null : gsel(el);
|
|
1748
|
+
const key = el.dataset.pixelshiftId || selector;
|
|
1749
|
+
|
|
1750
|
+
// Use provided prevProps if given (snapshot taken before style was applied),
|
|
1751
|
+
// otherwise snapshot current inline values (for tools like Colors/Typography that call rec before apply)
|
|
1752
|
+
const prevProps = prevPropsOverride || {};
|
|
1753
|
+
if (!prevPropsOverride) {
|
|
1754
|
+
Object.keys(props).forEach(p => {
|
|
1755
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1756
|
+
prevProps[p] = el.style[camel] || '';
|
|
1757
|
+
});
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
// Extract exact file from React Fiber if available
|
|
1761
|
+
function getReactSource(element) {
|
|
1762
|
+
for (const key in element) {
|
|
1763
|
+
if (key.startsWith('__reactFiber$')) {
|
|
1764
|
+
let fiber = element[key];
|
|
1765
|
+
while (fiber) {
|
|
1766
|
+
if (fiber._debugSource && fiber._debugSource.fileName) return fiber._debugSource.fileName;
|
|
1767
|
+
fiber = fiber.return;
|
|
1768
|
+
}
|
|
1667
1769
|
}
|
|
1668
1770
|
}
|
|
1771
|
+
return null;
|
|
1669
1772
|
}
|
|
1670
|
-
return null;
|
|
1671
|
-
}
|
|
1672
1773
|
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1774
|
+
// Merge into state.changes (for save/apply)
|
|
1775
|
+
const parentSel = isCreate && el.parentElement && el.parentElement !== document.body ? gsel(el.parentElement) : null;
|
|
1776
|
+
const ch = {
|
|
1777
|
+
type: isCreate ? 'create' : (el.dataset.pixelshiftId ? 'inline' : 'css'),
|
|
1778
|
+
isCreate,
|
|
1779
|
+
pixelshiftId: el.dataset.pixelshiftId || null,
|
|
1780
|
+
selector,
|
|
1781
|
+
parentSelector: parentSel,
|
|
1782
|
+
exactFile: getReactSource(el) || window.location.pathname.replace(/^\//, '') || null, // fallback for vanilla HTML (#7)
|
|
1783
|
+
file: el.dataset.pixelshiftFile || null,
|
|
1784
|
+
props,
|
|
1785
|
+
tagName: el.tagName.toLowerCase(),
|
|
1786
|
+
outerHTML: el.outerHTML // Send the whole element for 'create' actions
|
|
1787
|
+
};
|
|
1788
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1789
|
+
if (i >= 0) Object.assign(state.changes[i].props, props); else state.changes.push(ch);
|
|
1687
1790
|
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1791
|
+
// Deep check: only record if at least one property actually changed
|
|
1792
|
+
let changed = isCreate;
|
|
1793
|
+
if (!isCreate) {
|
|
1794
|
+
for (const p in props) {
|
|
1795
|
+
if (String(props[p]) !== String(prevProps[p])) { changed = true; break; }
|
|
1796
|
+
}
|
|
1693
1797
|
}
|
|
1798
|
+
if (!changed) return;
|
|
1799
|
+
|
|
1800
|
+
// Push to history
|
|
1801
|
+
const hid = Date.now() + Math.random();
|
|
1802
|
+
history.push({
|
|
1803
|
+
hid, el, props, prevProps, selector: key, isCreate,
|
|
1804
|
+
parent: isCreate ? el.parentElement : null,
|
|
1805
|
+
nextSibling: isCreate ? el.nextElementSibling : null
|
|
1806
|
+
});
|
|
1807
|
+
|
|
1808
|
+
// Clear redo stack on new action
|
|
1809
|
+
redoHistory.length = 0;
|
|
1810
|
+
|
|
1811
|
+
updateUnsUI();
|
|
1694
1812
|
}
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
updateUnsUI();
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
function updateUnsUI() {
|
|
1712
|
-
const n = state.changes.length;
|
|
1713
|
-
sv.disabled = n === 0; nb.textContent = history.length;
|
|
1714
|
-
uns.style.display = history.length ? 'flex' : 'none';
|
|
1715
|
-
|
|
1716
|
-
// Rebuild history list (newest first)
|
|
1717
|
-
unsList.innerHTML = '';
|
|
1718
|
-
[...history].reverse().forEach(h => {
|
|
1719
|
-
const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
1720
|
-
const row = document.createElement('div');
|
|
1721
|
-
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;';
|
|
1722
|
-
row.innerHTML = `
|
|
1813
|
+
|
|
1814
|
+
function updateUnsUI() {
|
|
1815
|
+
const n = state.changes.length;
|
|
1816
|
+
sv.disabled = n === 0; nb.textContent = history.length;
|
|
1817
|
+
uns.style.display = history.length ? 'flex' : 'none';
|
|
1818
|
+
|
|
1819
|
+
// Rebuild history list (newest first)
|
|
1820
|
+
unsList.innerHTML = '';
|
|
1821
|
+
[...history].reverse().forEach(h => {
|
|
1822
|
+
const propStr = Object.entries(h.props).map(([k, v]) => `${k}: ${v}`).join(', ');
|
|
1823
|
+
const row = document.createElement('div');
|
|
1824
|
+
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;';
|
|
1825
|
+
row.innerHTML = `
|
|
1723
1826
|
<div style="flex:1;overflow:hidden">
|
|
1724
1827
|
<div style="color:#7fff6e;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${h.selector || 'placed element'}</div>
|
|
1725
1828
|
<div style="color:#555577;margin-top:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${propStr}</div>
|
|
1726
1829
|
</div>
|
|
1727
1830
|
<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>
|
|
1728
1831
|
`;
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
|
-
function revertChange(h) {
|
|
1735
|
-
if (h.isCreate) {
|
|
1736
|
-
h.el.remove();
|
|
1737
|
-
} else {
|
|
1738
|
-
// Re-apply previous inline values
|
|
1739
|
-
Object.entries(h.prevProps).forEach(([prop, val]) => {
|
|
1740
|
-
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1741
|
-
h.el.style[camel] = val;
|
|
1832
|
+
row.querySelector('button').onclick = () => revertChange(h);
|
|
1833
|
+
unsList.appendChild(row);
|
|
1742
1834
|
});
|
|
1743
|
-
// Also handle text/html
|
|
1744
|
-
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1745
|
-
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1746
1835
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
history.splice(idx, 1);
|
|
1752
|
-
}
|
|
1753
|
-
// Rebuild state.changes — preserve all original fields (#1, #2)
|
|
1754
|
-
state.changes = [];
|
|
1755
|
-
history.forEach(x => {
|
|
1756
|
-
const key = x.selector;
|
|
1757
|
-
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1758
|
-
if (i >= 0) {
|
|
1759
|
-
Object.assign(state.changes[i].props, x.props);
|
|
1836
|
+
|
|
1837
|
+
function revertChange(h) {
|
|
1838
|
+
if (h.isCreate) {
|
|
1839
|
+
h.el.remove();
|
|
1760
1840
|
} else {
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
props: { ...x.props },
|
|
1766
|
-
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1767
|
-
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1841
|
+
// Re-apply previous inline values
|
|
1842
|
+
Object.entries(h.prevProps).forEach(([prop, val]) => {
|
|
1843
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1844
|
+
h.el.style[camel] = val;
|
|
1768
1845
|
});
|
|
1846
|
+
// Also handle text/html
|
|
1847
|
+
if (h.prevProps.innerHTML !== undefined) h.el.innerHTML = h.prevProps.innerHTML;
|
|
1848
|
+
if (h.prevProps.innerText !== undefined) h.el.innerText = h.prevProps.innerText;
|
|
1769
1849
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1850
|
+
// Remove from history and add to redo stack
|
|
1851
|
+
const idx = history.findIndex(x => x.hid === h.hid);
|
|
1852
|
+
if (idx >= 0) {
|
|
1853
|
+
redoHistory.push(history[idx]);
|
|
1854
|
+
history.splice(idx, 1);
|
|
1855
|
+
}
|
|
1856
|
+
// Rebuild state.changes — preserve all original fields (#1, #2)
|
|
1857
|
+
state.changes = [];
|
|
1858
|
+
history.forEach(x => {
|
|
1859
|
+
const key = x.selector;
|
|
1860
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1861
|
+
if (i >= 0) {
|
|
1862
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1863
|
+
} else {
|
|
1864
|
+
state.changes.push({
|
|
1865
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1866
|
+
isCreate: x.isCreate || false,
|
|
1867
|
+
selector: key,
|
|
1868
|
+
props: { ...x.props },
|
|
1869
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1870
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
});
|
|
1874
|
+
updateUnsUI();
|
|
1875
|
+
|
|
1876
|
+
if (state.selectedEl) {
|
|
1877
|
+
const isVisible = document.body.contains(state.selectedEl) && getComputedStyle(state.selectedEl).display !== 'none';
|
|
1878
|
+
if (isVisible) {
|
|
1879
|
+
if (state.tool === 'mov') placeHdl(state.selectedEl);
|
|
1880
|
+
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1881
|
+
} else if (state.selectedEl === h.el) {
|
|
1882
|
+
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1883
|
+
state.selectedEl = null;
|
|
1884
|
+
state.selectedEls = [];
|
|
1885
|
+
hdl.classList.remove('v');
|
|
1886
|
+
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
toast('↩ Reverted');
|
|
1779
1891
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1892
|
+
|
|
1893
|
+
function redoChange() {
|
|
1894
|
+
if (redoHistory.length === 0) {
|
|
1895
|
+
toast('Nothing to redo');
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
const h = redoHistory.pop();
|
|
1899
|
+
|
|
1900
|
+
if (h.isCreate) {
|
|
1901
|
+
if (h.parent) {
|
|
1902
|
+
if (h.nextSibling) {
|
|
1903
|
+
h.parent.insertBefore(h.el, h.nextSibling);
|
|
1904
|
+
} else {
|
|
1905
|
+
h.parent.appendChild(h.el);
|
|
1906
|
+
}
|
|
1786
1907
|
} else {
|
|
1787
|
-
|
|
1908
|
+
document.body.appendChild(h.el);
|
|
1788
1909
|
}
|
|
1789
1910
|
} else {
|
|
1790
|
-
|
|
1911
|
+
// Re-apply properties
|
|
1912
|
+
Object.entries(h.props).forEach(([prop, val]) => {
|
|
1913
|
+
const camel = prop.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
1914
|
+
h.el.style[camel] = val;
|
|
1915
|
+
});
|
|
1916
|
+
if (h.props.innerHTML !== undefined) h.el.innerHTML = h.props.innerHTML;
|
|
1917
|
+
if (h.props.innerText !== undefined) h.el.innerText = h.props.innerText;
|
|
1791
1918
|
}
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1919
|
+
|
|
1920
|
+
history.push(h);
|
|
1921
|
+
|
|
1922
|
+
// Rebuild state.changes
|
|
1923
|
+
state.changes = [];
|
|
1924
|
+
history.forEach(x => {
|
|
1925
|
+
const key = x.selector;
|
|
1926
|
+
const i = state.changes.findIndex(c => (c.pixelshiftId || c.selector) === key);
|
|
1927
|
+
if (i >= 0) {
|
|
1928
|
+
Object.assign(state.changes[i].props, x.props);
|
|
1929
|
+
} else {
|
|
1930
|
+
state.changes.push({
|
|
1931
|
+
type: x.isCreate ? 'create' : 'css',
|
|
1932
|
+
isCreate: x.isCreate || false,
|
|
1933
|
+
selector: key,
|
|
1934
|
+
props: { ...x.props },
|
|
1935
|
+
outerHTML: x.isCreate && x.el ? x.el.outerHTML : undefined,
|
|
1936
|
+
tagName: x.el ? x.el.tagName?.toLowerCase() : undefined
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1797
1939
|
});
|
|
1798
|
-
|
|
1799
|
-
|
|
1940
|
+
|
|
1941
|
+
updateUnsUI();
|
|
1942
|
+
|
|
1943
|
+
if (state.selectedEl) {
|
|
1944
|
+
const isVisible = document.body.contains(state.selectedEl) && getComputedStyle(state.selectedEl).display !== 'none';
|
|
1945
|
+
if (isVisible) {
|
|
1946
|
+
if (state.tool === 'mov') placeHdl(state.selectedEl);
|
|
1947
|
+
if (state.tool === 'rsz') placeRH(state.selectedEl);
|
|
1948
|
+
} else if (state.selectedEl === h.el) {
|
|
1949
|
+
state.selectedEl.classList.remove('__ps__', '__ps_multi__');
|
|
1950
|
+
state.selectedEl = null;
|
|
1951
|
+
state.selectedEls = [];
|
|
1952
|
+
hdl.classList.remove('v');
|
|
1953
|
+
Object.values(rhs).forEach(rh => rh.classList.remove('v'));
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
toast('↷ Redone');
|
|
1800
1958
|
}
|
|
1801
|
-
|
|
1802
|
-
history.push(h);
|
|
1803
1959
|
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1960
|
+
sv.addEventListener('click', async () => {
|
|
1961
|
+
// Check key config status
|
|
1962
|
+
let hasKey = false;
|
|
1963
|
+
let cfgProvider = 'groq';
|
|
1964
|
+
try {
|
|
1965
|
+
const cfgRes = await fetch('/draply-config');
|
|
1966
|
+
const cfg = await cfgRes.json();
|
|
1967
|
+
hasKey = cfg.hasKey;
|
|
1968
|
+
cfgProvider = cfg.provider || 'groq';
|
|
1969
|
+
} catch (e) { }
|
|
1970
|
+
|
|
1971
|
+
if (!hasKey) {
|
|
1972
|
+
// Ask for provider first (#8)
|
|
1973
|
+
const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
1974
|
+
if (!provider) { toast('Save aborted'); return; }
|
|
1975
|
+
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1976
|
+
if (key) {
|
|
1977
|
+
sv.disabled = true; sv.textContent = 'Validating...';
|
|
1978
|
+
try {
|
|
1979
|
+
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1980
|
+
const vData = await vRes.json();
|
|
1981
|
+
if (!vData.valid && provider !== 'ollama') {
|
|
1982
|
+
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1983
|
+
}
|
|
1984
|
+
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1985
|
+
|
|
1986
|
+
const configPayload = { apiKey: key.trim(), provider: provider.trim() };
|
|
1987
|
+
|
|
1988
|
+
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(configPayload) });
|
|
1989
|
+
sv.disabled = false; sv.textContent = 'Save';
|
|
1990
|
+
} else {
|
|
1991
|
+
toast('Save aborted: API Key required');
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// Progress indicator with animation (#9)
|
|
1997
|
+
sv.disabled = true;
|
|
1998
|
+
sv.textContent = '';
|
|
1999
|
+
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>';
|
|
2000
|
+
// Add spin keyframe if not exists
|
|
2001
|
+
if (!document.getElementById('__ps_spin_style__')) {
|
|
2002
|
+
const spinStyle = document.createElement('style');
|
|
2003
|
+
spinStyle.id = '__ps_spin_style__';
|
|
2004
|
+
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
2005
|
+
document.head.appendChild(spinStyle);
|
|
2006
|
+
}
|
|
2007
|
+
try {
|
|
2008
|
+
const r = await fetch('/draply-ai-apply', {
|
|
2009
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2010
|
+
body: JSON.stringify({ changes: state.changes })
|
|
1819
2011
|
});
|
|
2012
|
+
const d = await r.json();
|
|
2013
|
+
if (d.ok) {
|
|
2014
|
+
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
2015
|
+
toast(msg);
|
|
2016
|
+
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
2017
|
+
} catch {
|
|
2018
|
+
toast('⚠ Server unreachable');
|
|
1820
2019
|
}
|
|
2020
|
+
|
|
2021
|
+
state.changes = []; history.length = 0; redoHistory.length = 0;
|
|
2022
|
+
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
2023
|
+
updateUnsUI();
|
|
1821
2024
|
});
|
|
1822
2025
|
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
const cfg = await cfgRes.json();
|
|
1834
|
-
hasKey = cfg.hasKey;
|
|
1835
|
-
cfgProvider = cfg.provider || 'groq';
|
|
1836
|
-
} catch (e) { }
|
|
1837
|
-
|
|
1838
|
-
if (!hasKey) {
|
|
1839
|
-
// Ask for provider first (#8)
|
|
1840
|
-
const provider = prompt('Draply AI Save: Choose provider (groq / openai / anthropic / ollama):', 'groq');
|
|
1841
|
-
if (!provider) { toast('Save aborted'); return; }
|
|
2026
|
+
document.getElementById('__uns_clear__').onclick = () => {
|
|
2027
|
+
state.changes = []; history.length = 0; redoHistory.length = 0; updateUnsUI();
|
|
2028
|
+
toast('🗑 History cleared');
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
document.getElementById('__uns_save__').onclick = () => sv.click();
|
|
2032
|
+
|
|
2033
|
+
document.getElementById('__ps_ai_cfg__').onclick = async () => {
|
|
2034
|
+
const provider = prompt('Change AI Provider (groq / openai / anthropic / gemini / ollama):', 'groq');
|
|
2035
|
+
if (!provider) return;
|
|
1842
2036
|
const key = provider === 'ollama' ? 'local' : prompt(`Enter your ${provider.toUpperCase()} API key:`);
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1847
|
-
const vData = await vRes.json();
|
|
1848
|
-
if (!vData.valid && provider !== 'ollama') {
|
|
1849
|
-
toast('⚠ Invalid API key'); sv.disabled = false; sv.textContent = 'Save'; return;
|
|
1850
|
-
}
|
|
1851
|
-
} catch { /* allow through if validation endpoint unavailable */ }
|
|
1852
|
-
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
1853
|
-
sv.disabled = false; sv.textContent = 'Save';
|
|
1854
|
-
} else {
|
|
1855
|
-
toast('Save aborted: API Key required');
|
|
2037
|
+
|
|
2038
|
+
if (!key && provider !== 'ollama') {
|
|
2039
|
+
toast('⚠ API Key required');
|
|
1856
2040
|
return;
|
|
1857
2041
|
}
|
|
1858
|
-
}
|
|
1859
2042
|
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
spinStyle.textContent = '@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }';
|
|
1869
|
-
document.head.appendChild(spinStyle);
|
|
1870
|
-
}
|
|
1871
|
-
try {
|
|
1872
|
-
const r = await fetch('/draply-ai-apply', {
|
|
1873
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1874
|
-
body: JSON.stringify({ changes: state.changes })
|
|
1875
|
-
});
|
|
1876
|
-
const d = await r.json();
|
|
1877
|
-
if (d.ok) {
|
|
1878
|
-
const msg = d.fallback ? 'Saved to draply.css (No AI Key)' : `✅ AI Applied ${d.applied || ''}/${d.total || ''} to Source!`;
|
|
1879
|
-
toast(msg);
|
|
1880
|
-
} else toast('⚠ Error: ' + (d.error || 'unknown'));
|
|
1881
|
-
} catch {
|
|
1882
|
-
toast('⚠ Server unreachable');
|
|
1883
|
-
}
|
|
2043
|
+
toast('Verifying...');
|
|
2044
|
+
try {
|
|
2045
|
+
const vRes = await fetch('/draply-validate-key', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ apiKey: key.trim(), provider: provider.trim() }) });
|
|
2046
|
+
const vData = await vRes.json();
|
|
2047
|
+
if (!vData.valid && provider !== 'ollama') {
|
|
2048
|
+
toast('⚠ Invalid API key'); return;
|
|
2049
|
+
}
|
|
2050
|
+
} catch { /* ignore validation fail */ }
|
|
1884
2051
|
|
|
1885
|
-
|
|
1886
|
-
sv.innerHTML = 'Save'; sv.textContent = 'Save';
|
|
1887
|
-
updateUnsUI();
|
|
1888
|
-
});
|
|
2052
|
+
const configPayload = { apiKey: key.trim(), provider: provider.trim() };
|
|
1889
2053
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
};
|
|
2054
|
+
await fetch('/draply-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(configPayload) });
|
|
2055
|
+
toast('✅ AI Provider saved!');
|
|
2056
|
+
};
|
|
1894
2057
|
|
|
1895
|
-
|
|
2058
|
+
// ══════════════════════════════════════════
|
|
2059
|
+
// KEYBOARD SHORTCUTS (#4, #5, #15)
|
|
2060
|
+
// ══════════════════════════════════════════
|
|
2061
|
+
document.addEventListener('keydown', e => {
|
|
2062
|
+
// Ignore if typing in an input/textarea/contenteditable
|
|
2063
|
+
const tag = e.target.tagName.toLowerCase();
|
|
2064
|
+
if (tag === 'input' || tag === 'textarea' || e.target.isContentEditable) return;
|
|
1896
2065
|
|
|
1897
|
-
//
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2066
|
+
// Ctrl+Z — Undo last change (#4)
|
|
2067
|
+
if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
2068
|
+
e.preventDefault();
|
|
2069
|
+
if (history.length > 0) {
|
|
2070
|
+
revertChange(history[history.length - 1]);
|
|
2071
|
+
} else {
|
|
2072
|
+
toast('Nothing to undo');
|
|
2073
|
+
}
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
1904
2076
|
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
} else {
|
|
1911
|
-
toast('Nothing to undo');
|
|
2077
|
+
// Ctrl+Shift+Z — Redo (implemented)
|
|
2078
|
+
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'z') {
|
|
2079
|
+
e.preventDefault();
|
|
2080
|
+
redoChange();
|
|
2081
|
+
return;
|
|
1912
2082
|
}
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
2083
|
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2084
|
+
// Delete / Backspace — remove selected element (#5)
|
|
2085
|
+
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
2086
|
+
if (state.selectedEl && !ps(state.selectedEl)) {
|
|
2087
|
+
e.preventDefault();
|
|
2088
|
+
const el = state.selectedEl;
|
|
2089
|
+
// Record removal in history
|
|
2090
|
+
rec(el, { display: 'none' }, { display: el.style.display || '' });
|
|
2091
|
+
el.style.display = 'none';
|
|
2092
|
+
// Deselect
|
|
2093
|
+
el.classList.remove('__ps__', '__ps_multi__');
|
|
2094
|
+
state.selectedEl = null;
|
|
2095
|
+
state.selectedEls = [];
|
|
2096
|
+
hdl.classList.remove('v');
|
|
2097
|
+
toast('🗑 Element hidden (Delete)');
|
|
2098
|
+
}
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
1922
2101
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
2102
|
+
// Preview mode toggle: P key (#15)
|
|
2103
|
+
if (e.key === 'p' || e.key === 'P') {
|
|
2104
|
+
if (!e.ctrlKey && !e.metaKey) {
|
|
2105
|
+
e.preventDefault();
|
|
2106
|
+
togglePreview();
|
|
2107
|
+
}
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
// ══════════════════════════════════════════
|
|
2113
|
+
// PREVIEW MODE (#15)
|
|
2114
|
+
// ══════════════════════════════════════════
|
|
2115
|
+
let previewMode = false;
|
|
2116
|
+
function togglePreview() {
|
|
2117
|
+
previewMode = !previewMode;
|
|
2118
|
+
const psRoot = document.getElementById('__ps__');
|
|
2119
|
+
if (previewMode) {
|
|
2120
|
+
psRoot.style.display = 'none';
|
|
2121
|
+
// Remove all selection highlights
|
|
2122
|
+
document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
|
|
2123
|
+
el.classList.remove('__ps__', '__ps_multi__', '__ph__');
|
|
2124
|
+
});
|
|
1935
2125
|
hdl.classList.remove('v');
|
|
1936
|
-
|
|
2126
|
+
Object.values(rhs).forEach(h => h.classList.remove('v'));
|
|
2127
|
+
toast('👁 Preview mode — press P to exit');
|
|
2128
|
+
} else {
|
|
2129
|
+
psRoot.style.display = '';
|
|
2130
|
+
toast('Editor restored');
|
|
1937
2131
|
}
|
|
1938
|
-
return;
|
|
1939
2132
|
}
|
|
1940
2133
|
|
|
1941
|
-
//
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
2134
|
+
// ══════════════════════════════════════════
|
|
2135
|
+
// UTILS
|
|
2136
|
+
// ══════════════════════════════════════════
|
|
2137
|
+
function ps(el) { return el && el.closest('#__ps__'); }
|
|
2138
|
+
function gsel(el) {
|
|
2139
|
+
if (el.id) return '#' + el.id;
|
|
2140
|
+
// Build unique path up the DOM
|
|
2141
|
+
const parts = [];
|
|
2142
|
+
let cur = el;
|
|
2143
|
+
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
2144
|
+
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
2145
|
+
const tag = cur.tagName.toLowerCase();
|
|
2146
|
+
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
2147
|
+
const siblings = cur.parentElement
|
|
2148
|
+
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
2149
|
+
: [];
|
|
2150
|
+
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
2151
|
+
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
2152
|
+
cur = cur.parentElement;
|
|
1946
2153
|
}
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
// ══════════════════════════════════════════
|
|
1952
|
-
// PREVIEW MODE (#15)
|
|
1953
|
-
// ══════════════════════════════════════════
|
|
1954
|
-
let previewMode = false;
|
|
1955
|
-
function togglePreview() {
|
|
1956
|
-
previewMode = !previewMode;
|
|
1957
|
-
const psRoot = document.getElementById('__ps__');
|
|
1958
|
-
if (previewMode) {
|
|
1959
|
-
psRoot.style.display = 'none';
|
|
1960
|
-
// Remove all selection highlights
|
|
1961
|
-
document.querySelectorAll('.__ps__, .__ps_multi__, .__ph__').forEach(el => {
|
|
1962
|
-
el.classList.remove('__ps__', '__ps_multi__', '__ph__');
|
|
1963
|
-
});
|
|
1964
|
-
hdl.classList.remove('v');
|
|
1965
|
-
Object.values(rhs).forEach(h => h.classList.remove('v'));
|
|
1966
|
-
toast('👁 Preview mode — press P to exit');
|
|
1967
|
-
} else {
|
|
1968
|
-
psRoot.style.display = '';
|
|
1969
|
-
toast('Editor restored');
|
|
2154
|
+
return parts.join(' > ');
|
|
1970
2155
|
}
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
// UTILS
|
|
1975
|
-
// ══════════════════════════════════════════
|
|
1976
|
-
function ps(el) { return el && el.closest('#__ps__'); }
|
|
1977
|
-
function gsel(el) {
|
|
1978
|
-
if (el.id) return '#' + el.id;
|
|
1979
|
-
// Build unique path up the DOM
|
|
1980
|
-
const parts = [];
|
|
1981
|
-
let cur = el;
|
|
1982
|
-
while (cur && cur !== document.body && cur !== document.documentElement) {
|
|
1983
|
-
if (cur.id) { parts.unshift('#' + cur.id); break; }
|
|
1984
|
-
const tag = cur.tagName.toLowerCase();
|
|
1985
|
-
const cls = [...cur.classList].filter(c => !c.startsWith('__') && !c.startsWith('ps-')).join('.');
|
|
1986
|
-
const siblings = cur.parentElement
|
|
1987
|
-
? [...cur.parentElement.children].filter(c => c.tagName === cur.tagName)
|
|
1988
|
-
: [];
|
|
1989
|
-
const idx = siblings.length > 1 ? `:nth-of-type(${siblings.indexOf(cur) + 1})` : '';
|
|
1990
|
-
parts.unshift(cls ? `${tag}.${cls}${idx}` : `${tag}${idx}`);
|
|
1991
|
-
cur = cur.parentElement;
|
|
2156
|
+
function toast(msg) {
|
|
2157
|
+
tst.textContent = msg; tst.classList.add('v');
|
|
2158
|
+
clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
|
|
1992
2159
|
}
|
|
1993
|
-
|
|
1994
|
-
}
|
|
1995
|
-
function toast(msg) {
|
|
1996
|
-
tst.textContent = msg; tst.classList.add('v');
|
|
1997
|
-
clearTimeout(tst._t); tst._t = setTimeout(() => tst.classList.remove('v'), 2800);
|
|
1998
|
-
}
|
|
1999
|
-
}) ();
|
|
2160
|
+
})();
|