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