draply-dev 1.5.4 → 1.5.6

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