ngx-xtroedge-cms 1.3.17 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -304,6 +304,16 @@ var CMS_STYLES = `
304
304
  }
305
305
  .lcms-login-input:focus { border-color: var(--lcms-primary, #00C853); }
306
306
  .lcms-login-input::placeholder { color: rgba(255,255,255,0.25); }
307
+ .lcms-password-wrapper { position: relative; display: flex; align-items: center; }
308
+ .lcms-password-wrapper .lcms-login-input { padding-right: 40px; width: 100%; }
309
+ .lcms-password-toggle {
310
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
311
+ background: none; border: none !important; outline: none !important; cursor: pointer; padding: 4px;
312
+ color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
313
+ transition: color 0.2s; line-height: 0; z-index: 1;
314
+ }
315
+ .lcms-password-toggle:hover { color: rgba(0,0,0,0.8); }
316
+ .lcms-password-toggle svg { display: block; }
307
317
  .lcms-login-btn {
308
318
  width: 100%; padding: 11px; margin-top: 6px; border: none; border-radius: 8px; cursor: pointer;
309
319
  background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
@@ -319,8 +329,100 @@ var CMS_STYLES = `
319
329
  }
320
330
  .lcms-login-error.visible { display: block; }
321
331
 
332
+ /* LICENSE OVERLAY */
333
+ .lcms-license-overlay {
334
+ position: fixed; inset: 0; z-index: 10020;
335
+ display: flex; align-items: center; justify-content: center;
336
+ background: rgba(8, 8, 15, 0.85);
337
+ backdrop-filter: blur(20px) saturate(1.4);
338
+ -webkit-backdrop-filter: blur(20px) saturate(1.4);
339
+ font-family: system-ui, -apple-system, sans-serif;
340
+ }
341
+ .lcms-license-box {
342
+ background: #13151a; border: 1px solid rgba(255,255,255,0.1);
343
+ border-radius: 20px; padding: 40px 36px; width: 380px; text-align: center;
344
+ box-shadow: 0 24px 60px rgba(0,0,0,0.6);
345
+ }
346
+ .lcms-license-icon {
347
+ width: 56px; height: 56px; border-radius: 16px; margin: 0 auto 20px;
348
+ background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1));
349
+ border: 1px solid rgba(239,68,68,0.3);
350
+ display: flex; align-items: center; justify-content: center;
351
+ }
352
+ .lcms-license-icon.trial {
353
+ background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(251,191,36,0.1));
354
+ border-color: rgba(251,191,36,0.3);
355
+ }
356
+ .lcms-license-title {
357
+ color: white; font-size: 20px; font-weight: 700; margin-bottom: 8px;
358
+ }
359
+ .lcms-license-msg {
360
+ color: rgba(255,255,255,0.5); font-size: 13px; line-height: 1.6; margin-bottom: 24px;
361
+ }
362
+ .lcms-license-btn {
363
+ display: inline-block; padding: 11px 28px; border: none; border-radius: 8px; cursor: pointer;
364
+ background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
365
+ color: white; font-size: 13px; font-weight: 700; font-family: inherit;
366
+ letter-spacing: 0.3px; transition: filter 0.2s; text-decoration: none;
367
+ }
368
+ .lcms-license-btn:hover { filter: brightness(1.1); }
369
+ .lcms-license-sub {
370
+ color: rgba(255,255,255,0.3); font-size: 11px; margin-top: 16px;
371
+ }
372
+ .lcms-license-sub a { color: rgba(var(--lcms-primary-rgb, 0,200,83),0.7); text-decoration: none; }
373
+ .lcms-license-sub a:hover { text-decoration: underline; }
374
+
375
+ /* TRIAL BANNER */
376
+ .lcms-trial-banner {
377
+ display: flex; align-items: center; gap: 6px;
378
+ background: rgba(251,191,36,0.12); border: 1px solid rgba(251,191,36,0.3);
379
+ border-radius: 8px; padding: 6px 10px; margin-bottom: 10px;
380
+ font-size: 11px; font-weight: 600; color: #fbbf24;
381
+ }
382
+ .lcms-trial-banner-icon { font-size: 14px; }
383
+ .lcms-trial-banner-text { flex: 1; }
384
+ .lcms-trial-banner-dismiss {
385
+ background: none; border: none; color: rgba(251,191,36,0.5); cursor: pointer;
386
+ padding: 0; font-size: 14px; line-height: 1; transition: color 0.2s;
387
+ }
388
+ .lcms-trial-banner-dismiss:hover { color: #fbbf24; }
389
+
390
+ /* RICH TEXT TOOLBAR */
391
+ .lcms-rich-toolbar {
392
+ position: fixed; z-index: 10010;
393
+ display: flex; align-items: center; gap: 2px;
394
+ padding: 4px 6px;
395
+ background: rgba(30, 15, 60, 0.92);
396
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
397
+ border: 1px solid rgba(var(--lcms-primary-rgb, 0,200,83), 0.3);
398
+ border-radius: 8px;
399
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
400
+ opacity: 0; transform: translateY(8px);
401
+ transition: opacity 0.15s, transform 0.15s;
402
+ pointer-events: none;
403
+ font-family: system-ui, -apple-system, sans-serif;
404
+ }
405
+ .lcms-rich-toolbar.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
406
+ .lcms-rich-toolbar button {
407
+ width: 28px; height: 28px; border: none; border-radius: 4px;
408
+ background: transparent; color: #d0d0d0; cursor: pointer;
409
+ display: flex; align-items: center; justify-content: center;
410
+ font-size: 13px; font-weight: 700; padding: 0;
411
+ transition: background 0.15s, color 0.15s;
412
+ }
413
+ .lcms-rich-toolbar button:hover { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.3); color: #fff; }
414
+ .lcms-rich-toolbar button.active { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.5); color: #fff; }
415
+ .lcms-rich-toolbar .lcms-tb-sep { width: 1px; height: 18px; background: rgba(255,255,255,0.12); margin: 0 3px; flex-shrink: 0; }
416
+
322
417
  /* HIDDEN ELEMENTS */
323
418
  .lcms-hidden { display: none !important; }
419
+
420
+ /* EDIT MODE \u2014 keep animations running, just make CMS elements editable */
421
+ body.lcms-editing [data-cms] {
422
+ user-select: text !important;
423
+ -webkit-user-select: text !important;
424
+ cursor: text !important;
425
+ }
324
426
  `;
325
427
 
326
428
  // src/xtroedge-cms.ts
@@ -364,7 +466,7 @@ var DEFAULT_EDITABLE_TAGS = [
364
466
  "header",
365
467
  "footer"
366
468
  ];
367
- var XtroedgeCMS = class _XtroedgeCMS {
469
+ var _XtroedgeCMS = class _XtroedgeCMS {
368
470
  constructor(config) {
369
471
  this.brandingEl = null;
370
472
  this.siteIdentifier = "";
@@ -399,6 +501,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
399
501
  this.dirtyImageKeys = /* @__PURE__ */ new Set();
400
502
  this.registeredKeys = /* @__PURE__ */ new Set();
401
503
  // ===== DOM =====
504
+ this.domOriginals = {};
402
505
  this.managedElements = /* @__PURE__ */ new Map();
403
506
  this.managedImages = /* @__PURE__ */ new Map();
404
507
  this.autoDetectedElements = /* @__PURE__ */ new Set();
@@ -406,6 +509,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
406
509
  this.scanTimeout = null;
407
510
  this.activeImageEl = null;
408
511
  this.imageCtxMenu = null;
512
+ // ===== Rich Text Toolbar =====
513
+ this.richToolbarEl = null;
514
+ this.activeEditableEl = null;
515
+ this.toolbarHideTimeout = null;
516
+ this.selectionChangeHandler = null;
409
517
  // ===== Navigation =====
410
518
  this.currentSlug = "";
411
519
  this.currentTitle = "";
@@ -435,6 +543,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
435
543
  this.editModeContent = null;
436
544
  this.fileInput = null;
437
545
  this.imgOverlay = null;
546
+ // ===== Edit-mode visibility tracking =====
547
+ this.editScrollHandler = null;
548
+ this.editScrollRAF = null;
438
549
  // ===== FAB Drag =====
439
550
  this.posX = 20;
440
551
  this.posY = 20;
@@ -446,11 +557,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
446
557
  this.hasMoved = false;
447
558
  // ===== Timers =====
448
559
  this.toastTimer = null;
560
+ // ===== License =====
561
+ this.licenseValid = false;
562
+ this.licenseStatus = null;
563
+ this.licenseOverlayEl = null;
564
+ this.trialBannerDismissed = false;
449
565
  // ===== IndexedDB =====
450
566
  this.db = null;
451
567
  // ===== i18n cache =====
452
568
  this.translationCache = /* @__PURE__ */ new Map();
569
+ /** Set of positioned ancestors we added pointer-events:none to (for cleanup) */
570
+ this.modifiedAncestors = /* @__PURE__ */ new Set();
453
571
  this.config = config || {};
572
+ if (!this.config.apiBase) this.config.apiBase = "https://backend-xi-lime-d90e4p1ysf.vercel.app/api";
454
573
  this.containerSelector = this.config.containerSelector || "";
455
574
  this.editableTags = (this.config.editableTags || DEFAULT_EDITABLE_TAGS).map((t) => t.toLowerCase());
456
575
  this.languages = this.config.languages || ["en"];
@@ -459,6 +578,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
459
578
  this.historyRetentionMs = (this.config.historyRetentionDays || 7) * 24 * 60 * 60 * 1e3;
460
579
  this.currentLang = this.detectCurrentLanguage();
461
580
  this.siteIdentifier = this.resolveSiteIdentifier();
581
+ const savedClientApi = _XtroedgeCMS.secureGet("xtroedge_client_api");
582
+ if (savedClientApi) this.config.clientApi = savedClientApi;
462
583
  const savedColor = localStorage.getItem("xtroedge_theme_color");
463
584
  if (savedColor) this.highlightColor = savedColor;
464
585
  this.boundMouseMove = (e) => this.onDragMove(e);
@@ -468,13 +589,69 @@ var XtroedgeCMS = class _XtroedgeCMS {
468
589
  this.boundPopState = () => this.handleNavigation();
469
590
  this.boundHashChange = () => this.handleNavigation();
470
591
  }
592
+ static _enc(value) {
593
+ const k = _XtroedgeCMS._SK;
594
+ let result = "";
595
+ for (let i = 0; i < value.length; i++) {
596
+ result += String.fromCharCode(value.charCodeAt(i) ^ k.charCodeAt(i % k.length));
597
+ }
598
+ return btoa(result);
599
+ }
600
+ static _dec(encoded) {
601
+ try {
602
+ const k = _XtroedgeCMS._SK;
603
+ const decoded = atob(encoded);
604
+ let result = "";
605
+ for (let i = 0; i < decoded.length; i++) {
606
+ result += String.fromCharCode(decoded.charCodeAt(i) ^ k.charCodeAt(i % k.length));
607
+ }
608
+ return result;
609
+ } catch {
610
+ return "";
611
+ }
612
+ }
613
+ static _readVault() {
614
+ try {
615
+ const raw = localStorage.getItem(_XtroedgeCMS._VAULT_KEY);
616
+ if (!raw) return {};
617
+ return JSON.parse(_XtroedgeCMS._dec(raw));
618
+ } catch {
619
+ return {};
620
+ }
621
+ }
622
+ static _writeVault(vault) {
623
+ try {
624
+ localStorage.setItem(_XtroedgeCMS._VAULT_KEY, _XtroedgeCMS._enc(JSON.stringify(vault)));
625
+ } catch {
626
+ }
627
+ }
628
+ static secureSet(key, value) {
629
+ const v = _XtroedgeCMS._readVault();
630
+ v[key] = value;
631
+ _XtroedgeCMS._writeVault(v);
632
+ }
633
+ static secureGet(key) {
634
+ return _XtroedgeCMS._readVault()[key] || "";
635
+ }
636
+ static secureRemove(key) {
637
+ const v = _XtroedgeCMS._readVault();
638
+ delete v[key];
639
+ _XtroedgeCMS._writeVault(v);
640
+ }
641
+ static secureClear() {
642
+ try {
643
+ localStorage.removeItem(_XtroedgeCMS._VAULT_KEY);
644
+ } catch {
645
+ }
646
+ }
471
647
  // ===============================================
472
648
  // PUBLIC API
473
649
  // ===============================================
474
- init() {
475
- this.posY = window.innerHeight - 72;
650
+ async init() {
476
651
  this.injectStyles();
477
652
  this.applyThemeColor(this.highlightColor);
653
+ await this.validateLicense();
654
+ this.posY = window.innerHeight - 72;
478
655
  this.buildUI();
479
656
  this.interceptNavigation();
480
657
  this.observer = new MutationObserver(() => {
@@ -485,7 +662,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
485
662
  document.head.appendChild(this.styleEl);
486
663
  }
487
664
  if (this.scanTimeout) clearTimeout(this.scanTimeout);
488
- this.scanTimeout = setTimeout(() => this.autoDetectAndScan(), 150);
665
+ this.scanTimeout = setTimeout(() => {
666
+ this.autoDetectAndScan();
667
+ if (Object.keys(this.pageTexts).length > 0) {
668
+ this.updateElementTexts();
669
+ }
670
+ }, 150);
489
671
  });
490
672
  this.observer.observe(document.body, { childList: true, subtree: true });
491
673
  window.addEventListener("popstate", this.boundPopState);
@@ -496,6 +678,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
496
678
  this.cleanupManagedElements();
497
679
  this.styleEl?.remove();
498
680
  this.rootEl?.remove();
681
+ this.licenseOverlayEl?.remove();
499
682
  window.removeEventListener("popstate", this.boundPopState);
500
683
  window.removeEventListener("hashchange", this.boundHashChange);
501
684
  document.removeEventListener("mousemove", this.boundMouseMove);
@@ -509,6 +692,113 @@ var XtroedgeCMS = class _XtroedgeCMS {
509
692
  if (this.toastTimer) clearTimeout(this.toastTimer);
510
693
  this.db?.close();
511
694
  }
695
+ // 24 hours
696
+ async validateLicense() {
697
+ const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
698
+ if (!licenseKey) {
699
+ this.licenseStatus = { valid: false, plan: "invalid", message: "No license key provided." };
700
+ this.licenseValid = false;
701
+ return;
702
+ }
703
+ const cached = this.getLicenseCache();
704
+ if (cached) {
705
+ this.licenseStatus = cached;
706
+ this.licenseValid = cached.valid;
707
+ return;
708
+ }
709
+ const validateUrl = this.config.licenseValidateUrl || (this.config.apiBase ? `${this.config.apiBase}/license/validate` : "");
710
+ if (!validateUrl) {
711
+ this.licenseValid = true;
712
+ this.licenseStatus = { valid: true, plan: "trial", message: "No license API configured." };
713
+ return;
714
+ }
715
+ try {
716
+ const builderToken = _XtroedgeCMS.secureGet("builder_token") || "";
717
+ const headers = { "Content-Type": "application/json" };
718
+ if (builderToken) headers["Authorization"] = `Bearer ${builderToken}`;
719
+ const res = await fetch(validateUrl, {
720
+ method: "POST",
721
+ headers,
722
+ body: JSON.stringify({
723
+ licenseKey,
724
+ domain: window.location.hostname
725
+ })
726
+ });
727
+ let data;
728
+ try {
729
+ data = await res.json();
730
+ } catch {
731
+ throw new Error("Invalid response from license server");
732
+ }
733
+ this.licenseStatus = data;
734
+ this.setLicenseCache(data);
735
+ this.licenseValid = data.valid;
736
+ } catch {
737
+ const fallback = this.getLicenseCache(true);
738
+ if (fallback && fallback.valid) {
739
+ this.licenseValid = true;
740
+ this.licenseStatus = fallback;
741
+ } else {
742
+ this.licenseStatus = { valid: false, plan: "invalid", message: "Unable to verify license. Please check your internet connection." };
743
+ this.licenseValid = false;
744
+ }
745
+ }
746
+ }
747
+ getLicenseCache(ignoreTTL = false) {
748
+ try {
749
+ const raw = _XtroedgeCMS.secureGet(_XtroedgeCMS.LICENSE_CACHE_KEY);
750
+ if (!raw) return null;
751
+ const { status, timestamp, key } = JSON.parse(raw);
752
+ const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
753
+ if (key !== currentKey) return null;
754
+ if (!ignoreTTL && Date.now() - timestamp > _XtroedgeCMS.LICENSE_CACHE_TTL) return null;
755
+ return status;
756
+ } catch {
757
+ return null;
758
+ }
759
+ }
760
+ setLicenseCache(status) {
761
+ try {
762
+ const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
763
+ _XtroedgeCMS.secureSet(_XtroedgeCMS.LICENSE_CACHE_KEY, JSON.stringify({
764
+ status,
765
+ timestamp: Date.now(),
766
+ key: currentKey
767
+ }));
768
+ } catch {
769
+ }
770
+ }
771
+ /** Shows toast when user tries to edit with expired/invalid license */
772
+ showLicenseExpiredToast() {
773
+ const isExpired = this.licenseStatus?.plan === "expired";
774
+ const msg = isExpired ? "Your free trial has expired. Subscribe to enable editing." : this.licenseStatus?.message || "License validation failed. Editing is disabled.";
775
+ this.showToast(msg, "error");
776
+ }
777
+ /** Returns trial/expired banner HTML if applicable, or empty string */
778
+ getTrialBannerHTML() {
779
+ if (this.trialBannerDismissed || !this.licenseStatus) return "";
780
+ if (!this.licenseValid) {
781
+ const msg = this.licenseStatus.plan === "expired" ? "Trial expired \u2014 editing disabled. Subscribe to continue." : "License invalid \u2014 editing disabled.";
782
+ return `
783
+ <div class="lcms-trial-banner" id="lcms-trial-banner" style="background:rgba(239,68,68,0.12);border-color:rgba(239,68,68,0.3);color:#f87171;">
784
+ <span class="lcms-trial-banner-icon">\u{1F512}</span>
785
+ <span class="lcms-trial-banner-text">${msg}</span>
786
+ <button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss" style="color:rgba(239,68,68,0.5);">\u2715</button>
787
+ </div>
788
+ `;
789
+ }
790
+ if (this.licenseStatus.plan === "trial") {
791
+ const days = this.licenseStatus.daysLeft ?? 0;
792
+ return `
793
+ <div class="lcms-trial-banner" id="lcms-trial-banner">
794
+ <span class="lcms-trial-banner-icon">\u23F3</span>
795
+ <span class="lcms-trial-banner-text">Free Trial: ${days} day${days !== 1 ? "s" : ""} remaining</span>
796
+ <button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss">\u2715</button>
797
+ </div>
798
+ `;
799
+ }
800
+ return "";
801
+ }
512
802
  // ===============================================
513
803
  // STYLES INJECTION
514
804
  // ===============================================
@@ -564,7 +854,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
564
854
  const editFromSession = sessionStorage.getItem("builder_edit_mode") === "true";
565
855
  const wantsEdit = editViaParam || editFromSession;
566
856
  if (wantsEdit) {
567
- const token = localStorage.getItem("builder_token");
857
+ const token = _XtroedgeCMS.secureGet("builder_token");
568
858
  if (!token) {
569
859
  if (!this.pendingEditMode) {
570
860
  this.pendingEditMode = true;
@@ -577,7 +867,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
577
867
  }
578
868
  return;
579
869
  }
580
- this.editMode = true;
870
+ if (!this.licenseValid) {
871
+ this.showLicenseExpiredToast();
872
+ this.editMode = false;
873
+ } else {
874
+ this.editMode = true;
875
+ }
581
876
  }
582
877
  if (!slugChanged && this.initialized) {
583
878
  this.updateUI();
@@ -670,6 +965,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
670
965
  const isDomain = this.siteIdentifier.includes(".");
671
966
  this.siteIdEl.innerHTML = `<span class="lcms-site-id-icon">${isDomain ? "\u{1F310}" : "\u{1F511}"}</span><span class="lcms-site-id-text">${isDomain ? this.siteIdentifier : this.siteIdentifier.substring(0, 8)}</span>`;
672
967
  this.panelEl.appendChild(this.siteIdEl);
968
+ const trialHTML = this.getTrialBannerHTML();
969
+ if (trialHTML) {
970
+ const trialWrap = this.createElement("div", "");
971
+ trialWrap.innerHTML = trialHTML;
972
+ this.panelEl.appendChild(trialWrap);
973
+ setTimeout(() => {
974
+ const dismissBtn = document.getElementById("lcms-trial-dismiss");
975
+ dismissBtn?.addEventListener("click", (e) => {
976
+ e.stopPropagation();
977
+ this.trialBannerDismissed = true;
978
+ const banner = document.getElementById("lcms-trial-banner");
979
+ banner?.remove();
980
+ });
981
+ }, 0);
982
+ }
673
983
  this.editModeContent = this.createElement("div", "lcms-hidden");
674
984
  this.changesInfoEl = this.createElement("div", "lcms-changes-info lcms-hidden");
675
985
  this.editModeContent.appendChild(this.changesInfoEl);
@@ -874,8 +1184,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
874
1184
  let parent = el.parentElement;
875
1185
  while (parent && parent !== document.body) {
876
1186
  const tag = parent.tagName.toLowerCase();
877
- if (tag === "header" || tag.endsWith("-header")) return "/header";
878
- if (tag === "footer" || tag.endsWith("-footer")) return "/footer";
1187
+ if (tag.endsWith("-header")) return "/header";
1188
+ if (tag.endsWith("-footer")) return "/footer";
879
1189
  parent = parent.parentElement;
880
1190
  }
881
1191
  return this.currentSlug;
@@ -893,11 +1203,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
893
1203
  el.setAttribute("data-cms-section", sectionSlug);
894
1204
  this.autoDetectedElements.add(el);
895
1205
  };
1206
+ const RICH_TEXT_INLINE = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "SUB", "SUP", "MARK"]);
896
1207
  const selector = this.editableTags.join(",");
897
1208
  const elements = container.querySelectorAll(selector);
898
1209
  elements.forEach((el) => {
899
1210
  if (el.hasAttribute("data-cms")) return;
900
- if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
1211
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
1212
+ if (RICH_TEXT_INLINE.has(el.tagName) && el.parentElement?.closest("[data-cms]")) return;
901
1213
  if (el.children.length > 3) return;
902
1214
  const text = this.getDirectTextContent(el).trim();
903
1215
  if (text.length < 2) return;
@@ -906,14 +1218,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
906
1218
  const dataEditables = container.querySelectorAll("[data-editable]");
907
1219
  dataEditables.forEach((el) => {
908
1220
  if (el.hasAttribute("data-cms")) return;
909
- if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
1221
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
910
1222
  assignKey(el, getSectionSlug(el));
911
1223
  });
912
1224
  }
913
1225
  scanDOM() {
914
1226
  const elements = document.querySelectorAll("[data-cms]");
915
1227
  elements.forEach((el) => {
916
- if (el.closest("#xtroedge-cms-root")) return;
1228
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
917
1229
  const key = el.getAttribute("data-cms");
918
1230
  this.registeredKeys.add(key);
919
1231
  if (!this.managedElements.has(el)) {
@@ -961,7 +1273,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
961
1273
  hasEditableChildren(el) {
962
1274
  return !!el.querySelector("[data-cms]");
963
1275
  }
964
- setDirectTextContent(el, val) {
1276
+ getElementContent(el) {
1277
+ if (this.hasEditableChildren(el)) {
1278
+ return this.getDirectTextContent(el).trim();
1279
+ }
1280
+ return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
1281
+ }
1282
+ setElementContent(el, val) {
965
1283
  if (this.hasEditableChildren(el)) {
966
1284
  const textNodes = [];
967
1285
  for (let i = 0; i < el.childNodes.length; i++) {
@@ -969,28 +1287,38 @@ var XtroedgeCMS = class _XtroedgeCMS {
969
1287
  }
970
1288
  if (textNodes.length > 0) textNodes[0].textContent = val;
971
1289
  else el.prepend(document.createTextNode(val));
1290
+ } else if (this.richTextEnabled) {
1291
+ el.innerHTML = this.sanitizeHTML(val);
972
1292
  } else {
973
1293
  el.textContent = val;
974
1294
  }
975
1295
  }
1296
+ // Legacy alias for backward compat with internal calls
1297
+ setDirectTextContent(el, val) {
1298
+ this.setElementContent(el, val);
1299
+ }
976
1300
  // ===============================================
977
1301
  // ELEMENT EDITING
978
1302
  // ===============================================
979
1303
  attachElement(el, key) {
980
- const getText = () => this.hasEditableChildren(el) ? this.getDirectTextContent(el).trim() : el.textContent?.trim() || "";
1304
+ const getContent = () => this.getElementContent(el);
981
1305
  const blurHandler = () => {
982
- const text = getText();
1306
+ const text = getContent();
983
1307
  const currentVal = this.getPageText(key);
984
1308
  if (text !== currentVal) this.onTextChanged(key, text);
1309
+ this.hideRichToolbar();
985
1310
  };
986
1311
  const keydownHandler = (e) => {
987
1312
  if (e.key === "Enter") {
1313
+ if (this.richTextEnabled && !_XtroedgeCMS.SINGLE_LINE_TAGS.has(el.tagName)) {
1314
+ return;
1315
+ }
988
1316
  e.preventDefault();
989
1317
  el.blur();
990
1318
  }
991
1319
  };
992
1320
  const inputHandler = () => {
993
- const text = getText();
1321
+ const text = getContent();
994
1322
  const currentVal = this.getPageText(key);
995
1323
  if (text !== currentVal) this.onTextChanged(key, text);
996
1324
  };
@@ -1017,12 +1345,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
1017
1345
  this.managedElements.delete(el);
1018
1346
  }
1019
1347
  }
1348
+ /** Stop propagation so parent carousels (Swiper, Owl, etc.) don't hijack the event */
1349
+ static stopProp(e) {
1350
+ e.stopPropagation();
1351
+ }
1020
1352
  enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
1021
1353
  const val = this.getPageText(_key);
1022
- if (val) this.setDirectTextContent(el, val);
1354
+ if (val) this.setElementContent(el, val);
1023
1355
  el.setAttribute("contenteditable", "true");
1024
- el.style.outline = `2px dashed ${this.highlightColor}`;
1025
- el.style.outlineOffset = "-2px";
1356
+ el.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
1357
+ el.style.setProperty("outline-offset", "-2px", "important");
1026
1358
  el.style.cursor = "text";
1027
1359
  el.style.transition = "background 0.2s";
1028
1360
  el.style.minWidth = "20px";
@@ -1030,37 +1362,199 @@ var XtroedgeCMS = class _XtroedgeCMS {
1030
1362
  el.addEventListener("keydown", keyH);
1031
1363
  el.addEventListener("input", inputH);
1032
1364
  el.addEventListener("click", clickH, true);
1365
+ el.addEventListener("mousedown", _XtroedgeCMS.stopProp, true);
1366
+ el.addEventListener("touchstart", _XtroedgeCMS.stopProp, true);
1367
+ el.addEventListener("focus", () => this.showRichToolbar(el));
1033
1368
  }
1034
1369
  disableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
1035
1370
  el.removeAttribute("contenteditable");
1036
- el.style.outline = "";
1037
- el.style.outline = "";
1038
- el.style.outlineOffset = "";
1371
+ el.style.removeProperty("outline");
1372
+ el.style.removeProperty("outline-offset");
1039
1373
  el.style.cursor = "";
1040
1374
  el.style.transition = "";
1041
1375
  el.style.minWidth = "";
1042
1376
  const val = this.getPageText(_key);
1043
- if (val) this.setDirectTextContent(el, val);
1377
+ if (val) this.setElementContent(el, val);
1044
1378
  el.removeEventListener("blur", blurH);
1045
1379
  el.removeEventListener("keydown", keyH);
1046
1380
  el.removeEventListener("input", inputH);
1047
1381
  el.removeEventListener("click", clickH, true);
1382
+ el.removeEventListener("mousedown", _XtroedgeCMS.stopProp, true);
1383
+ el.removeEventListener("touchstart", _XtroedgeCMS.stopProp, true);
1048
1384
  }
1049
1385
  applyEditMode(editMode) {
1386
+ document.body.classList.toggle("lcms-editing", editMode);
1387
+ if (editMode) {
1388
+ this.editScrollHandler = () => {
1389
+ if (this.editScrollRAF) return;
1390
+ this.editScrollRAF = requestAnimationFrame(() => {
1391
+ this.syncEditablePointerEvents();
1392
+ this.editScrollRAF = null;
1393
+ });
1394
+ };
1395
+ window.addEventListener("scroll", this.editScrollHandler, { passive: true });
1396
+ this.syncEditablePointerEvents();
1397
+ } else {
1398
+ if (this.editScrollHandler) {
1399
+ window.removeEventListener("scroll", this.editScrollHandler);
1400
+ this.editScrollHandler = null;
1401
+ }
1402
+ if (this.editScrollRAF) {
1403
+ cancelAnimationFrame(this.editScrollRAF);
1404
+ this.editScrollRAF = null;
1405
+ }
1406
+ for (const [el] of this.managedElements) el.style.pointerEvents = "";
1407
+ this.modifiedAncestors.forEach((a) => {
1408
+ a.style.pointerEvents = "";
1409
+ });
1410
+ this.modifiedAncestors.clear();
1411
+ }
1050
1412
  for (const [el, info] of this.managedElements) {
1051
1413
  if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
1052
1414
  else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
1053
1415
  }
1054
1416
  this.applyImageEditMode(editMode);
1417
+ if (editMode) this.createRichToolbar();
1418
+ else this.destroyRichToolbar();
1419
+ }
1420
+ /**
1421
+ * For each managed element, find the nearest position:absolute/fixed ancestor.
1422
+ * If that ancestor is invisible (opacity ≈ 0), set pointer-events:none on it
1423
+ * so clicks pass through to the visible element behind it.
1424
+ */
1425
+ syncEditablePointerEvents() {
1426
+ const checked = /* @__PURE__ */ new Map();
1427
+ for (const [el] of this.managedElements) {
1428
+ const ancestor = this.findPositionedAncestor(el);
1429
+ if (!ancestor) continue;
1430
+ if (!checked.has(ancestor)) {
1431
+ const cs = window.getComputedStyle(ancestor);
1432
+ const visible = parseFloat(cs.opacity) >= 0.15 && cs.visibility !== "hidden" && cs.display !== "none";
1433
+ checked.set(ancestor, visible);
1434
+ if (!visible) {
1435
+ ancestor.style.pointerEvents = "none";
1436
+ this.modifiedAncestors.add(ancestor);
1437
+ } else {
1438
+ if (this.modifiedAncestors.has(ancestor)) {
1439
+ ancestor.style.pointerEvents = "";
1440
+ this.modifiedAncestors.delete(ancestor);
1441
+ }
1442
+ }
1443
+ }
1444
+ }
1445
+ }
1446
+ /** Walk up from el to find the nearest position:absolute or position:fixed ancestor */
1447
+ findPositionedAncestor(el) {
1448
+ let cur = el.parentElement;
1449
+ while (cur && cur !== document.body) {
1450
+ if (cur.closest("#xtroedge-cms-root")) return null;
1451
+ const pos = window.getComputedStyle(cur).position;
1452
+ if (pos === "absolute" || pos === "fixed") return cur;
1453
+ cur = cur.parentElement;
1454
+ }
1455
+ return null;
1456
+ }
1457
+ killAnimations() {
1458
+ const win = window;
1459
+ if (win.ScrollTrigger) {
1460
+ try {
1461
+ win.ScrollTrigger.getAll().forEach((t) => t.kill(true));
1462
+ } catch {
1463
+ }
1464
+ }
1465
+ if (win.gsap) {
1466
+ try {
1467
+ win.gsap.globalTimeline.clear();
1468
+ win.gsap.killTweensOf("*");
1469
+ } catch {
1470
+ }
1471
+ }
1472
+ if (win.AOS) {
1473
+ try {
1474
+ win.AOS.refreshHard?.();
1475
+ } catch {
1476
+ }
1477
+ }
1478
+ this.removePinSpacers();
1479
+ this.clearAnimationInlineStyles();
1480
+ setTimeout(() => {
1481
+ this.removePinSpacers();
1482
+ this.clearAnimationInlineStyles();
1483
+ }, 500);
1484
+ setTimeout(() => {
1485
+ this.removePinSpacers();
1486
+ this.clearAnimationInlineStyles();
1487
+ }, 1500);
1488
+ }
1489
+ /** Remove GSAP ScrollTrigger pin-spacer wrapper divs and restore original elements */
1490
+ removePinSpacers() {
1491
+ document.querySelectorAll(".pin-spacer").forEach((spacer) => {
1492
+ const child = spacer.firstElementChild;
1493
+ if (child) {
1494
+ spacer.parentNode?.insertBefore(child, spacer);
1495
+ child.style.cssText = "";
1496
+ }
1497
+ spacer.remove();
1498
+ });
1499
+ }
1500
+ /** Clear only animation-related inline styles (set by GSAP/ScrollTrigger) without breaking layout */
1501
+ clearAnimationInlineStyles() {
1502
+ const container = this.containerSelector ? document.querySelector(this.containerSelector) || document.body : document.body;
1503
+ container.querySelectorAll("*").forEach((el) => {
1504
+ if (el.closest("#xtroedge-cms-root")) return;
1505
+ const s = el.style;
1506
+ if (s.pointerEvents === "none") s.pointerEvents = "";
1507
+ if (s.userSelect === "none") s.userSelect = "";
1508
+ if (s.visibility === "hidden") s.visibility = "";
1509
+ if (s.clipPath && s.clipPath !== "none") s.clipPath = "";
1510
+ });
1511
+ }
1512
+ /** Capture each element's current DOM text before CMS applies saved edits */
1513
+ captureDomOriginals() {
1514
+ this.domOriginals = {};
1515
+ for (const [el, info] of this.managedElements) {
1516
+ this.domOriginals[info.key] = this.getElementContent(el);
1517
+ }
1518
+ }
1519
+ /** Ensure every entry in pageTexts has _orig for current language (from domOriginals) */
1520
+ ensureOriginals() {
1521
+ const origKey = `_orig_${this.currentLang}`;
1522
+ for (const key of Object.keys(this.pageTexts)) {
1523
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
1524
+ this.pageTexts[key][origKey] = this.domOriginals[key];
1525
+ }
1526
+ }
1527
+ }
1528
+ /** Find a managed element whose current text matches the given original text */
1529
+ findElementByOriginal(originalText, tagName) {
1530
+ for (const [el] of this.managedElements) {
1531
+ if (el.tagName !== tagName) continue;
1532
+ const current = this.getElementContent(el);
1533
+ if (current === originalText) return el;
1534
+ }
1535
+ return null;
1055
1536
  }
1056
1537
  updateElementTexts() {
1057
1538
  this.observer?.disconnect();
1539
+ let orphanedCount = 0;
1058
1540
  for (const [el, info] of this.managedElements) {
1059
1541
  if (document.activeElement === el) continue;
1060
- const val = this.getPageText(info.key);
1542
+ const entry = this.pageTexts[info.key];
1543
+ if (!entry) continue;
1544
+ const val = entry[this.currentLang];
1061
1545
  if (!val) continue;
1062
- const current = this.hasEditableChildren(el) ? this.getDirectTextContent(el).trim() : el.textContent?.trim() || "";
1063
- if (val !== current) this.setDirectTextContent(el, val);
1546
+ const orig = entry[`_orig_${this.currentLang}`];
1547
+ if (!orig) {
1548
+ const current = this.getElementContent(el);
1549
+ if (val !== current) this.setElementContent(el, val);
1550
+ continue;
1551
+ }
1552
+ const domText = this.domOriginals[info.key] || this.getElementContent(el);
1553
+ if (domText === orig) {
1554
+ if (val !== this.getElementContent(el)) this.setElementContent(el, val);
1555
+ } else {
1556
+ orphanedCount++;
1557
+ }
1064
1558
  }
1065
1559
  this.observer?.observe(document.body, { childList: true, subtree: true });
1066
1560
  }
@@ -1069,6 +1563,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1069
1563
  this.managedElements.clear();
1070
1564
  for (const el of this.autoDetectedElements) el.removeAttribute("data-cms");
1071
1565
  this.autoDetectedElements.clear();
1566
+ this.domOriginals = {};
1072
1567
  this.cleanupManagedImages();
1073
1568
  }
1074
1569
  // ===============================================
@@ -1088,7 +1583,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1088
1583
  const info = this.managedImages.get(img);
1089
1584
  if (info) {
1090
1585
  img.removeEventListener("contextmenu", info.ctxHandler);
1091
- img.style.outline = "";
1586
+ img.style.removeProperty("outline");
1092
1587
  img.style.cursor = "";
1093
1588
  this.managedImages.delete(img);
1094
1589
  }
@@ -1108,14 +1603,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
1108
1603
  enableImageEdit(img, ctxHandler) {
1109
1604
  if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
1110
1605
  img.title = "Right-click to change image";
1111
- img.style.outline = `2px dashed ${this.highlightColor}`;
1606
+ img.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
1112
1607
  img.style.cursor = "context-menu";
1113
1608
  img.addEventListener("contextmenu", ctxHandler);
1114
1609
  }
1115
1610
  disableImageEdit(img, ctxHandler) {
1116
1611
  img.title = img.dataset.origTitle || "";
1117
1612
  delete img.dataset.origTitle;
1118
- img.style.outline = "";
1613
+ img.style.removeProperty("outline");
1119
1614
  img.style.cursor = "";
1120
1615
  img.removeEventListener("contextmenu", ctxHandler);
1121
1616
  }
@@ -1164,7 +1659,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1164
1659
  this.dismissImageCtxMenu();
1165
1660
  });
1166
1661
  menu.appendChild(btn);
1167
- document.body.appendChild(menu);
1662
+ (this.rootEl || document.body).appendChild(menu);
1168
1663
  this.imageCtxMenu = menu;
1169
1664
  const closeHandler = (e) => {
1170
1665
  if (!menu.contains(e.target)) {
@@ -1195,14 +1690,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1195
1690
  }
1196
1691
  async uploadImageToApi(dataUrl) {
1197
1692
  try {
1198
- const res = await this.apiFetch(`${this.config.apiBase}/web/upload-image`, {
1693
+ const storedImageUrl = _XtroedgeCMS.secureGet("xtroedge_image_url");
1694
+ const clientBase = this.config.clientApi || this.config.apiBase;
1695
+ const uploadUrl = storedImageUrl ? this.resolveClientUrl(storedImageUrl) : `${clientBase}/web/upload-image`;
1696
+ const res = await this.apiFetch(uploadUrl, {
1199
1697
  method: "POST",
1200
1698
  headers: { "Content-Type": "application/json" },
1201
1699
  body: JSON.stringify({ url: dataUrl })
1202
1700
  });
1203
1701
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1204
1702
  const data = await res.json();
1205
- const baseUrl = this.config.imageBaseUrl || (this.config.apiBase ? new URL(this.config.apiBase).origin : "");
1703
+ const imgOriginBase = this.config.clientApi || this.config.apiBase || "";
1704
+ const baseUrl = this.config.imageBaseUrl || (imgOriginBase ? new URL(imgOriginBase).origin : "");
1206
1705
  const fullUrl = baseUrl + data.path;
1207
1706
  if (this.activeImageEl) {
1208
1707
  const key = this.managedImages.get(this.activeImageEl)?.key || "";
@@ -1239,6 +1738,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
1239
1738
  this.canUndo = true;
1240
1739
  this.canRedo = false;
1241
1740
  if (!this.pageTexts[key]) this.pageTexts[key] = {};
1741
+ const origKey = `_orig_${this.currentLang}`;
1742
+ if (!this.pageTexts[key][origKey]) {
1743
+ this.pageTexts[key][origKey] = this.domOriginals[key] || "";
1744
+ }
1242
1745
  this.pageTexts[key] = { ...this.pageTexts[key], [this.currentLang]: newValue };
1243
1746
  this.dirtyKeys.add(key);
1244
1747
  this.unsavedChanges = this.dirtyKeys.size + this.dirtyImageKeys.size;
@@ -1269,6 +1772,162 @@ var XtroedgeCMS = class _XtroedgeCMS {
1269
1772
  }
1270
1773
  }
1271
1774
  // ===============================================
1775
+ // RICH TEXT TOOLBAR
1776
+ // ===============================================
1777
+ get richTextEnabled() {
1778
+ return this.config.richText !== false;
1779
+ }
1780
+ sanitizeHTML(html) {
1781
+ const doc = new DOMParser().parseFromString(html, "text/html");
1782
+ const clean = (node) => {
1783
+ const children = Array.from(node.childNodes);
1784
+ for (const child of children) {
1785
+ if (child.nodeType === Node.TEXT_NODE) continue;
1786
+ if (child.nodeType === Node.ELEMENT_NODE) {
1787
+ const el = child;
1788
+ if (!_XtroedgeCMS.ALLOWED_TAGS.has(el.tagName)) {
1789
+ while (el.firstChild) el.parentNode?.insertBefore(el.firstChild, el);
1790
+ el.remove();
1791
+ } else {
1792
+ const allowedAttrs = _XtroedgeCMS.ALLOWED_ATTRS[el.tagName] || /* @__PURE__ */ new Set();
1793
+ for (const attr of Array.from(el.attributes)) {
1794
+ if (!allowedAttrs.has(attr.name)) el.removeAttribute(attr.name);
1795
+ }
1796
+ if (el.tagName === "A") {
1797
+ const href = el.getAttribute("href") || "";
1798
+ if (href.trim().toLowerCase().startsWith("javascript:")) {
1799
+ el.setAttribute("href", "#");
1800
+ }
1801
+ }
1802
+ clean(el);
1803
+ }
1804
+ } else {
1805
+ child.remove();
1806
+ }
1807
+ }
1808
+ };
1809
+ clean(doc.body);
1810
+ return doc.body.innerHTML;
1811
+ }
1812
+ createRichToolbar() {
1813
+ if (!this.richTextEnabled || this.richToolbarEl) return;
1814
+ const toolbar = document.createElement("div");
1815
+ toolbar.className = "lcms-rich-toolbar";
1816
+ const buttons = [
1817
+ { cmd: "bold", label: "B", title: "Bold" },
1818
+ { cmd: "italic", label: "<i>I</i>", title: "Italic" },
1819
+ { cmd: "underline", label: "<u>U</u>", title: "Underline" },
1820
+ { cmd: "strikeThrough", label: "<s>S</s>", title: "Strikethrough" }
1821
+ ];
1822
+ const linkButtons = [
1823
+ { cmd: "createLink", label: "\u{1F517}", title: "Insert Link" },
1824
+ { cmd: "unlink", label: "\u2298", title: "Remove Link" }
1825
+ ];
1826
+ const utilButtons = [
1827
+ { cmd: "removeFormat", label: "\u2715", title: "Clear Formatting" }
1828
+ ];
1829
+ const makeBtn = (item) => {
1830
+ const btn = document.createElement("button");
1831
+ btn.innerHTML = item.label;
1832
+ btn.title = item.title;
1833
+ btn.dataset.cmd = item.cmd;
1834
+ btn.addEventListener("mousedown", (e) => {
1835
+ e.preventDefault();
1836
+ this.execToolbarCommand(item.cmd);
1837
+ });
1838
+ return btn;
1839
+ };
1840
+ const addSep = () => {
1841
+ const sep = document.createElement("div");
1842
+ sep.className = "lcms-tb-sep";
1843
+ toolbar.appendChild(sep);
1844
+ };
1845
+ buttons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1846
+ addSep();
1847
+ linkButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1848
+ addSep();
1849
+ utilButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1850
+ this.rootEl?.appendChild(toolbar);
1851
+ this.richToolbarEl = toolbar;
1852
+ this.selectionChangeHandler = () => this.updateToolbarState();
1853
+ document.addEventListener("selectionchange", this.selectionChangeHandler);
1854
+ }
1855
+ destroyRichToolbar() {
1856
+ if (this.richToolbarEl) {
1857
+ this.richToolbarEl.remove();
1858
+ this.richToolbarEl = null;
1859
+ }
1860
+ if (this.selectionChangeHandler) {
1861
+ document.removeEventListener("selectionchange", this.selectionChangeHandler);
1862
+ this.selectionChangeHandler = null;
1863
+ }
1864
+ this.activeEditableEl = null;
1865
+ }
1866
+ execToolbarCommand(cmd) {
1867
+ this.observer?.disconnect();
1868
+ if (this.scanTimeout) {
1869
+ clearTimeout(this.scanTimeout);
1870
+ this.scanTimeout = null;
1871
+ }
1872
+ if (cmd === "createLink") {
1873
+ const url = prompt("Enter URL:", "https://");
1874
+ if (url) document.execCommand("createLink", false, url);
1875
+ } else {
1876
+ document.execCommand(cmd, false);
1877
+ }
1878
+ this.updateToolbarState();
1879
+ if (this.activeEditableEl) {
1880
+ this.activeEditableEl.dispatchEvent(new Event("input", { bubbles: true }));
1881
+ }
1882
+ setTimeout(() => {
1883
+ this.observer?.observe(document.body, { childList: true, subtree: true });
1884
+ }, 50);
1885
+ }
1886
+ showRichToolbar(el) {
1887
+ if (!this.richTextEnabled || !this.richToolbarEl) return;
1888
+ if (this.toolbarHideTimeout) {
1889
+ clearTimeout(this.toolbarHideTimeout);
1890
+ this.toolbarHideTimeout = null;
1891
+ }
1892
+ this.activeEditableEl = el;
1893
+ this.positionToolbar(el);
1894
+ this.richToolbarEl.classList.add("visible");
1895
+ this.updateToolbarState();
1896
+ }
1897
+ positionToolbar(el) {
1898
+ if (!this.richToolbarEl) return;
1899
+ const rect = el.getBoundingClientRect();
1900
+ const tbRect = this.richToolbarEl.getBoundingClientRect();
1901
+ let top = rect.top - tbRect.height - 8;
1902
+ let left = rect.left + rect.width / 2 - tbRect.width / 2;
1903
+ if (top < 4) top = rect.bottom + 8;
1904
+ if (left < 4) left = 4;
1905
+ if (left + tbRect.width > window.innerWidth - 4) left = window.innerWidth - tbRect.width - 4;
1906
+ this.richToolbarEl.style.top = `${top}px`;
1907
+ this.richToolbarEl.style.left = `${left}px`;
1908
+ }
1909
+ hideRichToolbar() {
1910
+ if (!this.richToolbarEl) return;
1911
+ this.toolbarHideTimeout = setTimeout(() => {
1912
+ this.richToolbarEl?.classList.remove("visible");
1913
+ this.activeEditableEl = null;
1914
+ this.toolbarHideTimeout = null;
1915
+ }, 150);
1916
+ }
1917
+ updateToolbarState() {
1918
+ if (!this.richToolbarEl) return;
1919
+ const cmds = ["bold", "italic", "underline", "strikeThrough"];
1920
+ for (const btn of Array.from(this.richToolbarEl.querySelectorAll("button"))) {
1921
+ const cmd = btn.dataset.cmd;
1922
+ if (cmd && cmds.includes(cmd)) {
1923
+ try {
1924
+ btn.classList.toggle("active", document.queryCommandState(cmd));
1925
+ } catch {
1926
+ }
1927
+ }
1928
+ }
1929
+ }
1930
+ // ===============================================
1272
1931
  // UNDO / REDO
1273
1932
  // ===============================================
1274
1933
  onUndo() {
@@ -1330,7 +1989,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1330
1989
  this.updateUI();
1331
1990
  }
1332
1991
  logout() {
1333
- localStorage.removeItem("builder_token");
1992
+ _XtroedgeCMS.secureClear();
1334
1993
  sessionStorage.removeItem("builder_edit_mode");
1335
1994
  this.editMode = false;
1336
1995
  this.isEditAllowed = false;
@@ -1356,6 +2015,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1356
2015
  }
1357
2016
  }
1358
2017
  this.autoDetectAndScan();
2018
+ this.captureDomOriginals();
1359
2019
  if (this.editMode) this.applyEditMode(true);
1360
2020
  if (this.config.apiBase) {
1361
2021
  if (loadDraft) this.loadPageContent("draft");
@@ -1387,17 +2047,40 @@ var XtroedgeCMS = class _XtroedgeCMS {
1387
2047
  getPageSection() {
1388
2048
  return this.currentSlug.replace(/^\//, "").replace(/-/g, "_").toUpperCase();
1389
2049
  }
1390
- // Authenticated fetch adds Authorization header from builder_token
2050
+ /** Resolve a stored URL against our server (apiBase) */
2051
+ resolveServerUrl(storedUrl) {
2052
+ if (storedUrl.startsWith("/")) return `${this.config.apiBase}${storedUrl}`;
2053
+ return storedUrl;
2054
+ }
2055
+ /** Resolve a stored URL against client's server (clientApi, falls back to apiBase) */
2056
+ resolveClientUrl(storedUrl) {
2057
+ const base = this.config.clientApi || this.config.apiBase;
2058
+ if (storedUrl.startsWith("/")) return `${base}${storedUrl}`;
2059
+ return storedUrl;
2060
+ }
1391
2061
  apiFetch(url, opts) {
1392
- const token = localStorage.getItem("builder_token") || "";
2062
+ const token = _XtroedgeCMS.secureGet("builder_token") || "";
2063
+ const licenseKey = this.config.licenseKey || "";
1393
2064
  const headers = { ...opts?.headers || {} };
1394
2065
  if (token) headers["Authorization"] = `Bearer ${token}`;
2066
+ if (licenseKey) headers["X-License-Key"] = licenseKey;
2067
+ const isOurServer = this.config.apiBase && url.startsWith(this.config.apiBase);
2068
+ if (!isOurServer && this.config.clientHeaders) {
2069
+ for (const [k, v] of Object.entries(this.config.clientHeaders)) {
2070
+ if (!headers[k]) headers[k] = v;
2071
+ }
2072
+ }
1395
2073
  return fetch(url, { ...opts, headers });
1396
2074
  }
1397
- // Fetch texts for a single slug and merge into pageTexts
2075
+ // Fetch page by slug if get_url exists in localStorage, call it directly (bypass our server)
1398
2076
  async fetchSection(slug, status) {
1399
2077
  try {
1400
- const res = await this.apiFetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(slug)}&status=${status}&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
2078
+ const storedGetUrl = _XtroedgeCMS.secureGet("xtroedge_get_url");
2079
+ const clientBase = this.config.clientApi || this.config.apiBase;
2080
+ const getBase = storedGetUrl ? this.resolveClientUrl(storedGetUrl) : `${clientBase}/web-page/get`;
2081
+ let getUrl = `${getBase}?slug=${encodeURIComponent(slug)}`;
2082
+ if (status) getUrl += `&status=${encodeURIComponent(status)}`;
2083
+ const res = await this.apiFetch(getUrl);
1401
2084
  if (!res.ok) return null;
1402
2085
  return await res.json();
1403
2086
  } catch {
@@ -1484,7 +2167,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
1484
2167
  for (const key of this.registeredKeys) {
1485
2168
  const el = document.querySelector(`[data-cms="${key}"]`);
1486
2169
  const val = el?.textContent?.trim() || "";
1487
- texts[key] = { [this.currentLang]: val };
2170
+ const entry = { [this.defaultLanguage]: val };
2171
+ texts[key] = entry;
1488
2172
  }
1489
2173
  this.pageTexts = texts;
1490
2174
  if (this.config.i18nBasePath) {
@@ -1523,12 +2207,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1523
2207
  return sections;
1524
2208
  }
1525
2209
  async saveChanges() {
2210
+ if (!this.licenseValid) {
2211
+ this.showLicenseExpiredToast();
2212
+ return;
2213
+ }
1526
2214
  if (!this.config.apiBase) {
1527
2215
  this.showToast("No API configured. Set apiBase to enable save.", "error");
1528
2216
  return;
1529
2217
  }
1530
2218
  this.isSaving = true;
1531
2219
  this.updateUI();
2220
+ const origKey = `_orig_${this.currentLang}`;
2221
+ for (const key of Object.keys(this.pageTexts)) {
2222
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
2223
+ this.pageTexts[key][origKey] = this.domOriginals[key];
2224
+ }
2225
+ }
1532
2226
  try {
1533
2227
  const sections = this.groupTextsBySection();
1534
2228
  const requests = Object.entries(sections).map(
@@ -1538,13 +2232,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1538
2232
  body: JSON.stringify({
1539
2233
  slug,
1540
2234
  title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1541
- site_identifier: this.siteIdentifier,
1542
2235
  content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1543
2236
  })
1544
2237
  })
1545
2238
  );
1546
- const results = await Promise.all(requests);
1547
- if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
2239
+ const responses = await Promise.all(requests);
2240
+ const saveResults = [];
2241
+ for (const res of responses) {
2242
+ const data = await res.json();
2243
+ saveResults.push({ ok: res.ok, status: res.status, data });
2244
+ }
2245
+ if (saveResults.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
2246
+ console.info("[XtroEdge] Save responses:", saveResults.map((r) => r.data));
1548
2247
  this.isSaving = false;
1549
2248
  this.resetAfterSave();
1550
2249
  this.updateUI();
@@ -1558,12 +2257,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1558
2257
  }
1559
2258
  }
1560
2259
  async publishChanges() {
2260
+ if (!this.licenseValid) {
2261
+ this.showLicenseExpiredToast();
2262
+ return;
2263
+ }
1561
2264
  if (!this.config.apiBase) {
1562
2265
  this.showToast("No API configured. Set apiBase to enable publish.", "error");
1563
2266
  return;
1564
2267
  }
1565
2268
  this.isPublishing = true;
1566
2269
  this.updateUI();
2270
+ const origKey = `_orig_${this.currentLang}`;
2271
+ for (const key of Object.keys(this.pageTexts)) {
2272
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
2273
+ this.pageTexts[key][origKey] = this.domOriginals[key];
2274
+ }
2275
+ }
1567
2276
  try {
1568
2277
  const sections = this.groupTextsBySection();
1569
2278
  const requests = Object.entries(sections).map(
@@ -1573,13 +2282,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1573
2282
  body: JSON.stringify({
1574
2283
  slug,
1575
2284
  title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1576
- site_identifier: this.siteIdentifier,
1577
2285
  published_content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1578
2286
  })
1579
2287
  })
1580
2288
  );
1581
2289
  const results = await Promise.all(requests);
1582
- if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
2290
+ const publishResults = [];
2291
+ for (const res of results) {
2292
+ const data = await res.json();
2293
+ publishResults.push({ ok: res.ok, status: res.status, data });
2294
+ }
2295
+ if (publishResults.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
2296
+ console.info("[XtroEdge] Publish responses:", publishResults.map((r) => r.data));
1583
2297
  this.isPublishing = false;
1584
2298
  this.resetAfterSave();
1585
2299
  this.updateUI();
@@ -1741,6 +2455,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
1741
2455
  toggleEditMode(e) {
1742
2456
  e.stopPropagation();
1743
2457
  const checked = e.target.checked;
2458
+ if (checked && !this.licenseValid) {
2459
+ e.target.checked = false;
2460
+ this.showLicenseExpiredToast();
2461
+ return;
2462
+ }
1744
2463
  if (checked) {
1745
2464
  sessionStorage.setItem("builder_edit_mode", "true");
1746
2465
  this.editMode = true;
@@ -1872,13 +2591,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
1872
2591
  </div>
1873
2592
  <div class="lcms-login-field">
1874
2593
  <label class="lcms-login-label">Password</label>
1875
- <input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
2594
+ <div class="lcms-password-wrapper">
2595
+ <input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
2596
+ <button type="button" class="lcms-password-toggle" id="lcms-password-toggle" tabindex="-1">
2597
+ <svg class="lcms-eye-open" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
2598
+ <svg class="lcms-eye-closed lcms-hidden" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>
2599
+ </button>
2600
+ </div>
1876
2601
  </div>
1877
2602
  <button class="lcms-login-btn" id="lcms-login-btn">Sign In</button>
1878
2603
  <div class="lcms-login-error" id="lcms-login-error"></div>
1879
2604
  </div>
1880
2605
  `;
1881
- document.body.appendChild(overlay);
2606
+ (this.rootEl || document.body).appendChild(overlay);
1882
2607
  this.loginModalEl = overlay;
1883
2608
  const btn = overlay.querySelector("#lcms-login-btn");
1884
2609
  const emailInput = overlay.querySelector("#lcms-login-email");
@@ -1888,6 +2613,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
1888
2613
  passInput.addEventListener("keydown", (e) => {
1889
2614
  if (e.key === "Enter") doLogin();
1890
2615
  });
2616
+ const toggleBtn = overlay.querySelector("#lcms-password-toggle");
2617
+ if (toggleBtn) {
2618
+ toggleBtn.addEventListener("click", () => {
2619
+ const isPassword = passInput.type === "password";
2620
+ passInput.type = isPassword ? "text" : "password";
2621
+ toggleBtn.querySelector(".lcms-eye-open").classList.toggle("lcms-hidden", !isPassword);
2622
+ toggleBtn.querySelector(".lcms-eye-closed").classList.toggle("lcms-hidden", isPassword);
2623
+ passInput.focus();
2624
+ });
2625
+ }
1891
2626
  setTimeout(() => emailInput.focus(), 50);
1892
2627
  }
1893
2628
  hideLoginModal() {
@@ -1923,7 +2658,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
1923
2658
  }
1924
2659
  const token = data?.token || data?.authToken || data?.auth_token || data?.data?.token;
1925
2660
  if (!token) throw new Error("Login successful but no token returned.");
1926
- localStorage.setItem("builder_token", token);
2661
+ _XtroedgeCMS.secureSet("builder_token", token);
2662
+ if (data?.get_url) _XtroedgeCMS.secureSet("xtroedge_get_url", data.get_url);
2663
+ else _XtroedgeCMS.secureRemove("xtroedge_get_url");
2664
+ if (data?.published_url) _XtroedgeCMS.secureSet("xtroedge_published_url", data.published_url);
2665
+ else _XtroedgeCMS.secureRemove("xtroedge_published_url");
2666
+ if (data?.image_url) _XtroedgeCMS.secureSet("xtroedge_image_url", data.image_url);
2667
+ else _XtroedgeCMS.secureRemove("xtroedge_image_url");
2668
+ if (data?.client_api) {
2669
+ _XtroedgeCMS.secureSet("xtroedge_client_api", data.client_api);
2670
+ this.config.clientApi = data.client_api;
2671
+ } else {
2672
+ _XtroedgeCMS.secureRemove("xtroedge_client_api");
2673
+ }
2674
+ if (data?.save_url) _XtroedgeCMS.secureSet("xtroedge_save_url", data.save_url);
2675
+ else _XtroedgeCMS.secureRemove("xtroedge_save_url");
1927
2676
  this.hideLoginModal();
1928
2677
  this.editMode = true;
1929
2678
  this.pendingEditMode = false;
@@ -1979,10 +2728,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
1979
2728
  document.documentElement.style.setProperty("--lcms-primary-dark", this.getDarkerColor(color));
1980
2729
  if (this.editMode) {
1981
2730
  for (const [el] of this.managedElements) {
1982
- el.style.outline = `2px dashed ${color}`;
2731
+ el.style.setProperty("outline", `2px dashed ${color}`, "important");
1983
2732
  }
1984
2733
  for (const [img] of this.managedImages) {
1985
- img.style.outline = `2px dashed ${color}`;
2734
+ img.style.setProperty("outline", `2px dashed ${color}`, "important");
1986
2735
  }
1987
2736
  }
1988
2737
  const swatches = this.panelEl?.querySelectorAll(".lcms-theme-swatch");
@@ -1995,23 +2744,71 @@ var XtroedgeCMS = class _XtroedgeCMS {
1995
2744
  // STATIC CONVENIENCE
1996
2745
  // ===============================================
1997
2746
  /** Quick init - create and start CMS in one call */
1998
- static create(config) {
2747
+ static async create(config) {
1999
2748
  const cms = new _XtroedgeCMS(config);
2000
- cms.init();
2749
+ await cms.init();
2001
2750
  return cms;
2002
2751
  }
2003
2752
  /** Auto-init: called automatically when package is loaded. No user code needed. */
2004
- static autoInit() {
2753
+ static async autoInit() {
2005
2754
  if (window.__xtroedge_cms_instance__) return;
2006
2755
  const userConfig = window.__XTROEDGE_CMS_CONFIG__ || {};
2007
- const cms = _XtroedgeCMS.create(userConfig);
2756
+ const cms = await _XtroedgeCMS.create(userConfig);
2008
2757
  window.__xtroedge_cms_instance__ = cms;
2009
2758
  }
2010
2759
  };
2760
+ // ===== Encrypted vault (single key stores all sensitive data) =====
2761
+ _XtroedgeCMS._SK = "xTr0EdG3_s3cUr3_k3y!@#2024";
2762
+ _XtroedgeCMS._VAULT_KEY = "_xtd";
2763
+ // ===============================================
2764
+ // LICENSE VALIDATION
2765
+ // ===============================================
2766
+ _XtroedgeCMS.LICENSE_CACHE_KEY = "xtroedge_license_cache";
2767
+ _XtroedgeCMS.LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
2768
+ // Tags where Enter should be blocked (single-line elements)
2769
+ _XtroedgeCMS.SINGLE_LINE_TAGS = /* @__PURE__ */ new Set([
2770
+ "H1",
2771
+ "H2",
2772
+ "H3",
2773
+ "H4",
2774
+ "H5",
2775
+ "H6",
2776
+ "BUTTON",
2777
+ "LABEL",
2778
+ "SPAN",
2779
+ "A",
2780
+ "SMALL",
2781
+ "B",
2782
+ "STRONG",
2783
+ "I",
2784
+ "EM"
2785
+ ]);
2786
+ // Allowed HTML tags for sanitization
2787
+ _XtroedgeCMS.ALLOWED_TAGS = /* @__PURE__ */ new Set([
2788
+ "B",
2789
+ "STRONG",
2790
+ "I",
2791
+ "EM",
2792
+ "U",
2793
+ "S",
2794
+ "STRIKE",
2795
+ "A",
2796
+ "BR",
2797
+ "UL",
2798
+ "OL",
2799
+ "LI",
2800
+ "SUB",
2801
+ "SUP"
2802
+ ]);
2803
+ // Allowed attributes per tag
2804
+ _XtroedgeCMS.ALLOWED_ATTRS = {
2805
+ A: /* @__PURE__ */ new Set(["href", "target", "rel"])
2806
+ };
2807
+ var XtroedgeCMS = _XtroedgeCMS;
2011
2808
 
2012
2809
  // src/index.ts
2013
- function boot() {
2014
- XtroedgeCMS.autoInit();
2810
+ async function boot() {
2811
+ await XtroedgeCMS.autoInit();
2015
2812
  }
2016
2813
  if (typeof window !== "undefined") {
2017
2814
  if (document.readyState === "loading") {