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.mjs CHANGED
@@ -278,6 +278,16 @@ var CMS_STYLES = `
278
278
  }
279
279
  .lcms-login-input:focus { border-color: var(--lcms-primary, #00C853); }
280
280
  .lcms-login-input::placeholder { color: rgba(255,255,255,0.25); }
281
+ .lcms-password-wrapper { position: relative; display: flex; align-items: center; }
282
+ .lcms-password-wrapper .lcms-login-input { padding-right: 40px; width: 100%; }
283
+ .lcms-password-toggle {
284
+ position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
285
+ background: none; border: none !important; outline: none !important; cursor: pointer; padding: 4px;
286
+ color: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
287
+ transition: color 0.2s; line-height: 0; z-index: 1;
288
+ }
289
+ .lcms-password-toggle:hover { color: rgba(0,0,0,0.8); }
290
+ .lcms-password-toggle svg { display: block; }
281
291
  .lcms-login-btn {
282
292
  width: 100%; padding: 11px; margin-top: 6px; border: none; border-radius: 8px; cursor: pointer;
283
293
  background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
@@ -293,8 +303,100 @@ var CMS_STYLES = `
293
303
  }
294
304
  .lcms-login-error.visible { display: block; }
295
305
 
306
+ /* LICENSE OVERLAY */
307
+ .lcms-license-overlay {
308
+ position: fixed; inset: 0; z-index: 10020;
309
+ display: flex; align-items: center; justify-content: center;
310
+ background: rgba(8, 8, 15, 0.85);
311
+ backdrop-filter: blur(20px) saturate(1.4);
312
+ -webkit-backdrop-filter: blur(20px) saturate(1.4);
313
+ font-family: system-ui, -apple-system, sans-serif;
314
+ }
315
+ .lcms-license-box {
316
+ background: #13151a; border: 1px solid rgba(255,255,255,0.1);
317
+ border-radius: 20px; padding: 40px 36px; width: 380px; text-align: center;
318
+ box-shadow: 0 24px 60px rgba(0,0,0,0.6);
319
+ }
320
+ .lcms-license-icon {
321
+ width: 56px; height: 56px; border-radius: 16px; margin: 0 auto 20px;
322
+ background: linear-gradient(135deg, rgba(239,68,68,0.2), rgba(239,68,68,0.1));
323
+ border: 1px solid rgba(239,68,68,0.3);
324
+ display: flex; align-items: center; justify-content: center;
325
+ }
326
+ .lcms-license-icon.trial {
327
+ background: linear-gradient(135deg, rgba(251,191,36,0.2), rgba(251,191,36,0.1));
328
+ border-color: rgba(251,191,36,0.3);
329
+ }
330
+ .lcms-license-title {
331
+ color: white; font-size: 20px; font-weight: 700; margin-bottom: 8px;
332
+ }
333
+ .lcms-license-msg {
334
+ color: rgba(255,255,255,0.5); font-size: 13px; line-height: 1.6; margin-bottom: 24px;
335
+ }
336
+ .lcms-license-btn {
337
+ display: inline-block; padding: 11px 28px; border: none; border-radius: 8px; cursor: pointer;
338
+ background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
339
+ color: white; font-size: 13px; font-weight: 700; font-family: inherit;
340
+ letter-spacing: 0.3px; transition: filter 0.2s; text-decoration: none;
341
+ }
342
+ .lcms-license-btn:hover { filter: brightness(1.1); }
343
+ .lcms-license-sub {
344
+ color: rgba(255,255,255,0.3); font-size: 11px; margin-top: 16px;
345
+ }
346
+ .lcms-license-sub a { color: rgba(var(--lcms-primary-rgb, 0,200,83),0.7); text-decoration: none; }
347
+ .lcms-license-sub a:hover { text-decoration: underline; }
348
+
349
+ /* TRIAL BANNER */
350
+ .lcms-trial-banner {
351
+ display: flex; align-items: center; gap: 6px;
352
+ background: rgba(251,191,36,0.12); border: 1px solid rgba(251,191,36,0.3);
353
+ border-radius: 8px; padding: 6px 10px; margin-bottom: 10px;
354
+ font-size: 11px; font-weight: 600; color: #fbbf24;
355
+ }
356
+ .lcms-trial-banner-icon { font-size: 14px; }
357
+ .lcms-trial-banner-text { flex: 1; }
358
+ .lcms-trial-banner-dismiss {
359
+ background: none; border: none; color: rgba(251,191,36,0.5); cursor: pointer;
360
+ padding: 0; font-size: 14px; line-height: 1; transition: color 0.2s;
361
+ }
362
+ .lcms-trial-banner-dismiss:hover { color: #fbbf24; }
363
+
364
+ /* RICH TEXT TOOLBAR */
365
+ .lcms-rich-toolbar {
366
+ position: fixed; z-index: 10010;
367
+ display: flex; align-items: center; gap: 2px;
368
+ padding: 4px 6px;
369
+ background: rgba(30, 15, 60, 0.92);
370
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
371
+ border: 1px solid rgba(var(--lcms-primary-rgb, 0,200,83), 0.3);
372
+ border-radius: 8px;
373
+ box-shadow: 0 4px 20px rgba(0,0,0,0.3);
374
+ opacity: 0; transform: translateY(8px);
375
+ transition: opacity 0.15s, transform 0.15s;
376
+ pointer-events: none;
377
+ font-family: system-ui, -apple-system, sans-serif;
378
+ }
379
+ .lcms-rich-toolbar.visible { opacity: 1; transform: translateY(0); pointer-events: auto; }
380
+ .lcms-rich-toolbar button {
381
+ width: 28px; height: 28px; border: none; border-radius: 4px;
382
+ background: transparent; color: #d0d0d0; cursor: pointer;
383
+ display: flex; align-items: center; justify-content: center;
384
+ font-size: 13px; font-weight: 700; padding: 0;
385
+ transition: background 0.15s, color 0.15s;
386
+ }
387
+ .lcms-rich-toolbar button:hover { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.3); color: #fff; }
388
+ .lcms-rich-toolbar button.active { background: rgba(var(--lcms-primary-rgb, 0,200,83), 0.5); color: #fff; }
389
+ .lcms-rich-toolbar .lcms-tb-sep { width: 1px; height: 18px; background: rgba(255,255,255,0.12); margin: 0 3px; flex-shrink: 0; }
390
+
296
391
  /* HIDDEN ELEMENTS */
297
392
  .lcms-hidden { display: none !important; }
393
+
394
+ /* EDIT MODE \u2014 keep animations running, just make CMS elements editable */
395
+ body.lcms-editing [data-cms] {
396
+ user-select: text !important;
397
+ -webkit-user-select: text !important;
398
+ cursor: text !important;
399
+ }
298
400
  `;
299
401
 
300
402
  // src/xtroedge-cms.ts
@@ -338,7 +440,7 @@ var DEFAULT_EDITABLE_TAGS = [
338
440
  "header",
339
441
  "footer"
340
442
  ];
341
- var XtroedgeCMS = class _XtroedgeCMS {
443
+ var _XtroedgeCMS = class _XtroedgeCMS {
342
444
  constructor(config) {
343
445
  this.brandingEl = null;
344
446
  this.siteIdentifier = "";
@@ -373,6 +475,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
373
475
  this.dirtyImageKeys = /* @__PURE__ */ new Set();
374
476
  this.registeredKeys = /* @__PURE__ */ new Set();
375
477
  // ===== DOM =====
478
+ this.domOriginals = {};
376
479
  this.managedElements = /* @__PURE__ */ new Map();
377
480
  this.managedImages = /* @__PURE__ */ new Map();
378
481
  this.autoDetectedElements = /* @__PURE__ */ new Set();
@@ -380,6 +483,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
380
483
  this.scanTimeout = null;
381
484
  this.activeImageEl = null;
382
485
  this.imageCtxMenu = null;
486
+ // ===== Rich Text Toolbar =====
487
+ this.richToolbarEl = null;
488
+ this.activeEditableEl = null;
489
+ this.toolbarHideTimeout = null;
490
+ this.selectionChangeHandler = null;
383
491
  // ===== Navigation =====
384
492
  this.currentSlug = "";
385
493
  this.currentTitle = "";
@@ -409,6 +517,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
409
517
  this.editModeContent = null;
410
518
  this.fileInput = null;
411
519
  this.imgOverlay = null;
520
+ // ===== Edit-mode visibility tracking =====
521
+ this.editScrollHandler = null;
522
+ this.editScrollRAF = null;
412
523
  // ===== FAB Drag =====
413
524
  this.posX = 20;
414
525
  this.posY = 20;
@@ -420,11 +531,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
420
531
  this.hasMoved = false;
421
532
  // ===== Timers =====
422
533
  this.toastTimer = null;
534
+ // ===== License =====
535
+ this.licenseValid = false;
536
+ this.licenseStatus = null;
537
+ this.licenseOverlayEl = null;
538
+ this.trialBannerDismissed = false;
423
539
  // ===== IndexedDB =====
424
540
  this.db = null;
425
541
  // ===== i18n cache =====
426
542
  this.translationCache = /* @__PURE__ */ new Map();
543
+ /** Set of positioned ancestors we added pointer-events:none to (for cleanup) */
544
+ this.modifiedAncestors = /* @__PURE__ */ new Set();
427
545
  this.config = config || {};
546
+ if (!this.config.apiBase) this.config.apiBase = "https://backend-xi-lime-d90e4p1ysf.vercel.app/api";
428
547
  this.containerSelector = this.config.containerSelector || "";
429
548
  this.editableTags = (this.config.editableTags || DEFAULT_EDITABLE_TAGS).map((t) => t.toLowerCase());
430
549
  this.languages = this.config.languages || ["en"];
@@ -433,6 +552,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
433
552
  this.historyRetentionMs = (this.config.historyRetentionDays || 7) * 24 * 60 * 60 * 1e3;
434
553
  this.currentLang = this.detectCurrentLanguage();
435
554
  this.siteIdentifier = this.resolveSiteIdentifier();
555
+ const savedClientApi = _XtroedgeCMS.secureGet("xtroedge_client_api");
556
+ if (savedClientApi) this.config.clientApi = savedClientApi;
436
557
  const savedColor = localStorage.getItem("xtroedge_theme_color");
437
558
  if (savedColor) this.highlightColor = savedColor;
438
559
  this.boundMouseMove = (e) => this.onDragMove(e);
@@ -442,13 +563,69 @@ var XtroedgeCMS = class _XtroedgeCMS {
442
563
  this.boundPopState = () => this.handleNavigation();
443
564
  this.boundHashChange = () => this.handleNavigation();
444
565
  }
566
+ static _enc(value) {
567
+ const k = _XtroedgeCMS._SK;
568
+ let result = "";
569
+ for (let i = 0; i < value.length; i++) {
570
+ result += String.fromCharCode(value.charCodeAt(i) ^ k.charCodeAt(i % k.length));
571
+ }
572
+ return btoa(result);
573
+ }
574
+ static _dec(encoded) {
575
+ try {
576
+ const k = _XtroedgeCMS._SK;
577
+ const decoded = atob(encoded);
578
+ let result = "";
579
+ for (let i = 0; i < decoded.length; i++) {
580
+ result += String.fromCharCode(decoded.charCodeAt(i) ^ k.charCodeAt(i % k.length));
581
+ }
582
+ return result;
583
+ } catch {
584
+ return "";
585
+ }
586
+ }
587
+ static _readVault() {
588
+ try {
589
+ const raw = localStorage.getItem(_XtroedgeCMS._VAULT_KEY);
590
+ if (!raw) return {};
591
+ return JSON.parse(_XtroedgeCMS._dec(raw));
592
+ } catch {
593
+ return {};
594
+ }
595
+ }
596
+ static _writeVault(vault) {
597
+ try {
598
+ localStorage.setItem(_XtroedgeCMS._VAULT_KEY, _XtroedgeCMS._enc(JSON.stringify(vault)));
599
+ } catch {
600
+ }
601
+ }
602
+ static secureSet(key, value) {
603
+ const v = _XtroedgeCMS._readVault();
604
+ v[key] = value;
605
+ _XtroedgeCMS._writeVault(v);
606
+ }
607
+ static secureGet(key) {
608
+ return _XtroedgeCMS._readVault()[key] || "";
609
+ }
610
+ static secureRemove(key) {
611
+ const v = _XtroedgeCMS._readVault();
612
+ delete v[key];
613
+ _XtroedgeCMS._writeVault(v);
614
+ }
615
+ static secureClear() {
616
+ try {
617
+ localStorage.removeItem(_XtroedgeCMS._VAULT_KEY);
618
+ } catch {
619
+ }
620
+ }
445
621
  // ===============================================
446
622
  // PUBLIC API
447
623
  // ===============================================
448
- init() {
449
- this.posY = window.innerHeight - 72;
624
+ async init() {
450
625
  this.injectStyles();
451
626
  this.applyThemeColor(this.highlightColor);
627
+ await this.validateLicense();
628
+ this.posY = window.innerHeight - 72;
452
629
  this.buildUI();
453
630
  this.interceptNavigation();
454
631
  this.observer = new MutationObserver(() => {
@@ -459,7 +636,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
459
636
  document.head.appendChild(this.styleEl);
460
637
  }
461
638
  if (this.scanTimeout) clearTimeout(this.scanTimeout);
462
- this.scanTimeout = setTimeout(() => this.autoDetectAndScan(), 150);
639
+ this.scanTimeout = setTimeout(() => {
640
+ this.autoDetectAndScan();
641
+ if (Object.keys(this.pageTexts).length > 0) {
642
+ this.updateElementTexts();
643
+ }
644
+ }, 150);
463
645
  });
464
646
  this.observer.observe(document.body, { childList: true, subtree: true });
465
647
  window.addEventListener("popstate", this.boundPopState);
@@ -470,6 +652,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
470
652
  this.cleanupManagedElements();
471
653
  this.styleEl?.remove();
472
654
  this.rootEl?.remove();
655
+ this.licenseOverlayEl?.remove();
473
656
  window.removeEventListener("popstate", this.boundPopState);
474
657
  window.removeEventListener("hashchange", this.boundHashChange);
475
658
  document.removeEventListener("mousemove", this.boundMouseMove);
@@ -483,6 +666,113 @@ var XtroedgeCMS = class _XtroedgeCMS {
483
666
  if (this.toastTimer) clearTimeout(this.toastTimer);
484
667
  this.db?.close();
485
668
  }
669
+ // 24 hours
670
+ async validateLicense() {
671
+ const licenseKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
672
+ if (!licenseKey) {
673
+ this.licenseStatus = { valid: false, plan: "invalid", message: "No license key provided." };
674
+ this.licenseValid = false;
675
+ return;
676
+ }
677
+ const cached = this.getLicenseCache();
678
+ if (cached) {
679
+ this.licenseStatus = cached;
680
+ this.licenseValid = cached.valid;
681
+ return;
682
+ }
683
+ const validateUrl = this.config.licenseValidateUrl || (this.config.apiBase ? `${this.config.apiBase}/license/validate` : "");
684
+ if (!validateUrl) {
685
+ this.licenseValid = true;
686
+ this.licenseStatus = { valid: true, plan: "trial", message: "No license API configured." };
687
+ return;
688
+ }
689
+ try {
690
+ const builderToken = _XtroedgeCMS.secureGet("builder_token") || "";
691
+ const headers = { "Content-Type": "application/json" };
692
+ if (builderToken) headers["Authorization"] = `Bearer ${builderToken}`;
693
+ const res = await fetch(validateUrl, {
694
+ method: "POST",
695
+ headers,
696
+ body: JSON.stringify({
697
+ licenseKey,
698
+ domain: window.location.hostname
699
+ })
700
+ });
701
+ let data;
702
+ try {
703
+ data = await res.json();
704
+ } catch {
705
+ throw new Error("Invalid response from license server");
706
+ }
707
+ this.licenseStatus = data;
708
+ this.setLicenseCache(data);
709
+ this.licenseValid = data.valid;
710
+ } catch {
711
+ const fallback = this.getLicenseCache(true);
712
+ if (fallback && fallback.valid) {
713
+ this.licenseValid = true;
714
+ this.licenseStatus = fallback;
715
+ } else {
716
+ this.licenseStatus = { valid: false, plan: "invalid", message: "Unable to verify license. Please check your internet connection." };
717
+ this.licenseValid = false;
718
+ }
719
+ }
720
+ }
721
+ getLicenseCache(ignoreTTL = false) {
722
+ try {
723
+ const raw = _XtroedgeCMS.secureGet(_XtroedgeCMS.LICENSE_CACHE_KEY);
724
+ if (!raw) return null;
725
+ const { status, timestamp, key } = JSON.parse(raw);
726
+ const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
727
+ if (key !== currentKey) return null;
728
+ if (!ignoreTTL && Date.now() - timestamp > _XtroedgeCMS.LICENSE_CACHE_TTL) return null;
729
+ return status;
730
+ } catch {
731
+ return null;
732
+ }
733
+ }
734
+ setLicenseCache(status) {
735
+ try {
736
+ const currentKey = this.config.licenseKey || _XtroedgeCMS.secureGet("builder_token") || "";
737
+ _XtroedgeCMS.secureSet(_XtroedgeCMS.LICENSE_CACHE_KEY, JSON.stringify({
738
+ status,
739
+ timestamp: Date.now(),
740
+ key: currentKey
741
+ }));
742
+ } catch {
743
+ }
744
+ }
745
+ /** Shows toast when user tries to edit with expired/invalid license */
746
+ showLicenseExpiredToast() {
747
+ const isExpired = this.licenseStatus?.plan === "expired";
748
+ const msg = isExpired ? "Your free trial has expired. Subscribe to enable editing." : this.licenseStatus?.message || "License validation failed. Editing is disabled.";
749
+ this.showToast(msg, "error");
750
+ }
751
+ /** Returns trial/expired banner HTML if applicable, or empty string */
752
+ getTrialBannerHTML() {
753
+ if (this.trialBannerDismissed || !this.licenseStatus) return "";
754
+ if (!this.licenseValid) {
755
+ const msg = this.licenseStatus.plan === "expired" ? "Trial expired \u2014 editing disabled. Subscribe to continue." : "License invalid \u2014 editing disabled.";
756
+ return `
757
+ <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;">
758
+ <span class="lcms-trial-banner-icon">\u{1F512}</span>
759
+ <span class="lcms-trial-banner-text">${msg}</span>
760
+ <button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss" style="color:rgba(239,68,68,0.5);">\u2715</button>
761
+ </div>
762
+ `;
763
+ }
764
+ if (this.licenseStatus.plan === "trial") {
765
+ const days = this.licenseStatus.daysLeft ?? 0;
766
+ return `
767
+ <div class="lcms-trial-banner" id="lcms-trial-banner">
768
+ <span class="lcms-trial-banner-icon">\u23F3</span>
769
+ <span class="lcms-trial-banner-text">Free Trial: ${days} day${days !== 1 ? "s" : ""} remaining</span>
770
+ <button class="lcms-trial-banner-dismiss" id="lcms-trial-dismiss" title="Dismiss">\u2715</button>
771
+ </div>
772
+ `;
773
+ }
774
+ return "";
775
+ }
486
776
  // ===============================================
487
777
  // STYLES INJECTION
488
778
  // ===============================================
@@ -538,7 +828,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
538
828
  const editFromSession = sessionStorage.getItem("builder_edit_mode") === "true";
539
829
  const wantsEdit = editViaParam || editFromSession;
540
830
  if (wantsEdit) {
541
- const token = localStorage.getItem("builder_token");
831
+ const token = _XtroedgeCMS.secureGet("builder_token");
542
832
  if (!token) {
543
833
  if (!this.pendingEditMode) {
544
834
  this.pendingEditMode = true;
@@ -551,7 +841,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
551
841
  }
552
842
  return;
553
843
  }
554
- this.editMode = true;
844
+ if (!this.licenseValid) {
845
+ this.showLicenseExpiredToast();
846
+ this.editMode = false;
847
+ } else {
848
+ this.editMode = true;
849
+ }
555
850
  }
556
851
  if (!slugChanged && this.initialized) {
557
852
  this.updateUI();
@@ -644,6 +939,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
644
939
  const isDomain = this.siteIdentifier.includes(".");
645
940
  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>`;
646
941
  this.panelEl.appendChild(this.siteIdEl);
942
+ const trialHTML = this.getTrialBannerHTML();
943
+ if (trialHTML) {
944
+ const trialWrap = this.createElement("div", "");
945
+ trialWrap.innerHTML = trialHTML;
946
+ this.panelEl.appendChild(trialWrap);
947
+ setTimeout(() => {
948
+ const dismissBtn = document.getElementById("lcms-trial-dismiss");
949
+ dismissBtn?.addEventListener("click", (e) => {
950
+ e.stopPropagation();
951
+ this.trialBannerDismissed = true;
952
+ const banner = document.getElementById("lcms-trial-banner");
953
+ banner?.remove();
954
+ });
955
+ }, 0);
956
+ }
647
957
  this.editModeContent = this.createElement("div", "lcms-hidden");
648
958
  this.changesInfoEl = this.createElement("div", "lcms-changes-info lcms-hidden");
649
959
  this.editModeContent.appendChild(this.changesInfoEl);
@@ -848,8 +1158,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
848
1158
  let parent = el.parentElement;
849
1159
  while (parent && parent !== document.body) {
850
1160
  const tag = parent.tagName.toLowerCase();
851
- if (tag === "header" || tag.endsWith("-header")) return "/header";
852
- if (tag === "footer" || tag.endsWith("-footer")) return "/footer";
1161
+ if (tag.endsWith("-header")) return "/header";
1162
+ if (tag.endsWith("-footer")) return "/footer";
853
1163
  parent = parent.parentElement;
854
1164
  }
855
1165
  return this.currentSlug;
@@ -867,11 +1177,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
867
1177
  el.setAttribute("data-cms-section", sectionSlug);
868
1178
  this.autoDetectedElements.add(el);
869
1179
  };
1180
+ const RICH_TEXT_INLINE = /* @__PURE__ */ new Set(["B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "SUB", "SUP", "MARK"]);
870
1181
  const selector = this.editableTags.join(",");
871
1182
  const elements = container.querySelectorAll(selector);
872
1183
  elements.forEach((el) => {
873
1184
  if (el.hasAttribute("data-cms")) return;
874
- if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
1185
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
1186
+ if (RICH_TEXT_INLINE.has(el.tagName) && el.parentElement?.closest("[data-cms]")) return;
875
1187
  if (el.children.length > 3) return;
876
1188
  const text = this.getDirectTextContent(el).trim();
877
1189
  if (text.length < 2) return;
@@ -880,14 +1192,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
880
1192
  const dataEditables = container.querySelectorAll("[data-editable]");
881
1193
  dataEditables.forEach((el) => {
882
1194
  if (el.hasAttribute("data-cms")) return;
883
- if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
1195
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal, script, style, noscript")) return;
884
1196
  assignKey(el, getSectionSlug(el));
885
1197
  });
886
1198
  }
887
1199
  scanDOM() {
888
1200
  const elements = document.querySelectorAll("[data-cms]");
889
1201
  elements.forEach((el) => {
890
- if (el.closest("#xtroedge-cms-root")) return;
1202
+ if (el.closest("#xtroedge-cms-root, #lcms-login-modal")) return;
891
1203
  const key = el.getAttribute("data-cms");
892
1204
  this.registeredKeys.add(key);
893
1205
  if (!this.managedElements.has(el)) {
@@ -935,7 +1247,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
935
1247
  hasEditableChildren(el) {
936
1248
  return !!el.querySelector("[data-cms]");
937
1249
  }
938
- setDirectTextContent(el, val) {
1250
+ getElementContent(el) {
1251
+ if (this.hasEditableChildren(el)) {
1252
+ return this.getDirectTextContent(el).trim();
1253
+ }
1254
+ return this.richTextEnabled ? el.innerHTML?.trim() || "" : el.textContent?.trim() || "";
1255
+ }
1256
+ setElementContent(el, val) {
939
1257
  if (this.hasEditableChildren(el)) {
940
1258
  const textNodes = [];
941
1259
  for (let i = 0; i < el.childNodes.length; i++) {
@@ -943,28 +1261,38 @@ var XtroedgeCMS = class _XtroedgeCMS {
943
1261
  }
944
1262
  if (textNodes.length > 0) textNodes[0].textContent = val;
945
1263
  else el.prepend(document.createTextNode(val));
1264
+ } else if (this.richTextEnabled) {
1265
+ el.innerHTML = this.sanitizeHTML(val);
946
1266
  } else {
947
1267
  el.textContent = val;
948
1268
  }
949
1269
  }
1270
+ // Legacy alias for backward compat with internal calls
1271
+ setDirectTextContent(el, val) {
1272
+ this.setElementContent(el, val);
1273
+ }
950
1274
  // ===============================================
951
1275
  // ELEMENT EDITING
952
1276
  // ===============================================
953
1277
  attachElement(el, key) {
954
- const getText = () => this.hasEditableChildren(el) ? this.getDirectTextContent(el).trim() : el.textContent?.trim() || "";
1278
+ const getContent = () => this.getElementContent(el);
955
1279
  const blurHandler = () => {
956
- const text = getText();
1280
+ const text = getContent();
957
1281
  const currentVal = this.getPageText(key);
958
1282
  if (text !== currentVal) this.onTextChanged(key, text);
1283
+ this.hideRichToolbar();
959
1284
  };
960
1285
  const keydownHandler = (e) => {
961
1286
  if (e.key === "Enter") {
1287
+ if (this.richTextEnabled && !_XtroedgeCMS.SINGLE_LINE_TAGS.has(el.tagName)) {
1288
+ return;
1289
+ }
962
1290
  e.preventDefault();
963
1291
  el.blur();
964
1292
  }
965
1293
  };
966
1294
  const inputHandler = () => {
967
- const text = getText();
1295
+ const text = getContent();
968
1296
  const currentVal = this.getPageText(key);
969
1297
  if (text !== currentVal) this.onTextChanged(key, text);
970
1298
  };
@@ -991,12 +1319,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
991
1319
  this.managedElements.delete(el);
992
1320
  }
993
1321
  }
1322
+ /** Stop propagation so parent carousels (Swiper, Owl, etc.) don't hijack the event */
1323
+ static stopProp(e) {
1324
+ e.stopPropagation();
1325
+ }
994
1326
  enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
995
1327
  const val = this.getPageText(_key);
996
- if (val) this.setDirectTextContent(el, val);
1328
+ if (val) this.setElementContent(el, val);
997
1329
  el.setAttribute("contenteditable", "true");
998
- el.style.outline = `2px dashed ${this.highlightColor}`;
999
- el.style.outlineOffset = "-2px";
1330
+ el.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
1331
+ el.style.setProperty("outline-offset", "-2px", "important");
1000
1332
  el.style.cursor = "text";
1001
1333
  el.style.transition = "background 0.2s";
1002
1334
  el.style.minWidth = "20px";
@@ -1004,37 +1336,199 @@ var XtroedgeCMS = class _XtroedgeCMS {
1004
1336
  el.addEventListener("keydown", keyH);
1005
1337
  el.addEventListener("input", inputH);
1006
1338
  el.addEventListener("click", clickH, true);
1339
+ el.addEventListener("mousedown", _XtroedgeCMS.stopProp, true);
1340
+ el.addEventListener("touchstart", _XtroedgeCMS.stopProp, true);
1341
+ el.addEventListener("focus", () => this.showRichToolbar(el));
1007
1342
  }
1008
1343
  disableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
1009
1344
  el.removeAttribute("contenteditable");
1010
- el.style.outline = "";
1011
- el.style.outline = "";
1012
- el.style.outlineOffset = "";
1345
+ el.style.removeProperty("outline");
1346
+ el.style.removeProperty("outline-offset");
1013
1347
  el.style.cursor = "";
1014
1348
  el.style.transition = "";
1015
1349
  el.style.minWidth = "";
1016
1350
  const val = this.getPageText(_key);
1017
- if (val) this.setDirectTextContent(el, val);
1351
+ if (val) this.setElementContent(el, val);
1018
1352
  el.removeEventListener("blur", blurH);
1019
1353
  el.removeEventListener("keydown", keyH);
1020
1354
  el.removeEventListener("input", inputH);
1021
1355
  el.removeEventListener("click", clickH, true);
1356
+ el.removeEventListener("mousedown", _XtroedgeCMS.stopProp, true);
1357
+ el.removeEventListener("touchstart", _XtroedgeCMS.stopProp, true);
1022
1358
  }
1023
1359
  applyEditMode(editMode) {
1360
+ document.body.classList.toggle("lcms-editing", editMode);
1361
+ if (editMode) {
1362
+ this.editScrollHandler = () => {
1363
+ if (this.editScrollRAF) return;
1364
+ this.editScrollRAF = requestAnimationFrame(() => {
1365
+ this.syncEditablePointerEvents();
1366
+ this.editScrollRAF = null;
1367
+ });
1368
+ };
1369
+ window.addEventListener("scroll", this.editScrollHandler, { passive: true });
1370
+ this.syncEditablePointerEvents();
1371
+ } else {
1372
+ if (this.editScrollHandler) {
1373
+ window.removeEventListener("scroll", this.editScrollHandler);
1374
+ this.editScrollHandler = null;
1375
+ }
1376
+ if (this.editScrollRAF) {
1377
+ cancelAnimationFrame(this.editScrollRAF);
1378
+ this.editScrollRAF = null;
1379
+ }
1380
+ for (const [el] of this.managedElements) el.style.pointerEvents = "";
1381
+ this.modifiedAncestors.forEach((a) => {
1382
+ a.style.pointerEvents = "";
1383
+ });
1384
+ this.modifiedAncestors.clear();
1385
+ }
1024
1386
  for (const [el, info] of this.managedElements) {
1025
1387
  if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
1026
1388
  else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
1027
1389
  }
1028
1390
  this.applyImageEditMode(editMode);
1391
+ if (editMode) this.createRichToolbar();
1392
+ else this.destroyRichToolbar();
1393
+ }
1394
+ /**
1395
+ * For each managed element, find the nearest position:absolute/fixed ancestor.
1396
+ * If that ancestor is invisible (opacity ≈ 0), set pointer-events:none on it
1397
+ * so clicks pass through to the visible element behind it.
1398
+ */
1399
+ syncEditablePointerEvents() {
1400
+ const checked = /* @__PURE__ */ new Map();
1401
+ for (const [el] of this.managedElements) {
1402
+ const ancestor = this.findPositionedAncestor(el);
1403
+ if (!ancestor) continue;
1404
+ if (!checked.has(ancestor)) {
1405
+ const cs = window.getComputedStyle(ancestor);
1406
+ const visible = parseFloat(cs.opacity) >= 0.15 && cs.visibility !== "hidden" && cs.display !== "none";
1407
+ checked.set(ancestor, visible);
1408
+ if (!visible) {
1409
+ ancestor.style.pointerEvents = "none";
1410
+ this.modifiedAncestors.add(ancestor);
1411
+ } else {
1412
+ if (this.modifiedAncestors.has(ancestor)) {
1413
+ ancestor.style.pointerEvents = "";
1414
+ this.modifiedAncestors.delete(ancestor);
1415
+ }
1416
+ }
1417
+ }
1418
+ }
1419
+ }
1420
+ /** Walk up from el to find the nearest position:absolute or position:fixed ancestor */
1421
+ findPositionedAncestor(el) {
1422
+ let cur = el.parentElement;
1423
+ while (cur && cur !== document.body) {
1424
+ if (cur.closest("#xtroedge-cms-root")) return null;
1425
+ const pos = window.getComputedStyle(cur).position;
1426
+ if (pos === "absolute" || pos === "fixed") return cur;
1427
+ cur = cur.parentElement;
1428
+ }
1429
+ return null;
1430
+ }
1431
+ killAnimations() {
1432
+ const win = window;
1433
+ if (win.ScrollTrigger) {
1434
+ try {
1435
+ win.ScrollTrigger.getAll().forEach((t) => t.kill(true));
1436
+ } catch {
1437
+ }
1438
+ }
1439
+ if (win.gsap) {
1440
+ try {
1441
+ win.gsap.globalTimeline.clear();
1442
+ win.gsap.killTweensOf("*");
1443
+ } catch {
1444
+ }
1445
+ }
1446
+ if (win.AOS) {
1447
+ try {
1448
+ win.AOS.refreshHard?.();
1449
+ } catch {
1450
+ }
1451
+ }
1452
+ this.removePinSpacers();
1453
+ this.clearAnimationInlineStyles();
1454
+ setTimeout(() => {
1455
+ this.removePinSpacers();
1456
+ this.clearAnimationInlineStyles();
1457
+ }, 500);
1458
+ setTimeout(() => {
1459
+ this.removePinSpacers();
1460
+ this.clearAnimationInlineStyles();
1461
+ }, 1500);
1462
+ }
1463
+ /** Remove GSAP ScrollTrigger pin-spacer wrapper divs and restore original elements */
1464
+ removePinSpacers() {
1465
+ document.querySelectorAll(".pin-spacer").forEach((spacer) => {
1466
+ const child = spacer.firstElementChild;
1467
+ if (child) {
1468
+ spacer.parentNode?.insertBefore(child, spacer);
1469
+ child.style.cssText = "";
1470
+ }
1471
+ spacer.remove();
1472
+ });
1473
+ }
1474
+ /** Clear only animation-related inline styles (set by GSAP/ScrollTrigger) without breaking layout */
1475
+ clearAnimationInlineStyles() {
1476
+ const container = this.containerSelector ? document.querySelector(this.containerSelector) || document.body : document.body;
1477
+ container.querySelectorAll("*").forEach((el) => {
1478
+ if (el.closest("#xtroedge-cms-root")) return;
1479
+ const s = el.style;
1480
+ if (s.pointerEvents === "none") s.pointerEvents = "";
1481
+ if (s.userSelect === "none") s.userSelect = "";
1482
+ if (s.visibility === "hidden") s.visibility = "";
1483
+ if (s.clipPath && s.clipPath !== "none") s.clipPath = "";
1484
+ });
1485
+ }
1486
+ /** Capture each element's current DOM text before CMS applies saved edits */
1487
+ captureDomOriginals() {
1488
+ this.domOriginals = {};
1489
+ for (const [el, info] of this.managedElements) {
1490
+ this.domOriginals[info.key] = this.getElementContent(el);
1491
+ }
1492
+ }
1493
+ /** Ensure every entry in pageTexts has _orig for current language (from domOriginals) */
1494
+ ensureOriginals() {
1495
+ const origKey = `_orig_${this.currentLang}`;
1496
+ for (const key of Object.keys(this.pageTexts)) {
1497
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
1498
+ this.pageTexts[key][origKey] = this.domOriginals[key];
1499
+ }
1500
+ }
1501
+ }
1502
+ /** Find a managed element whose current text matches the given original text */
1503
+ findElementByOriginal(originalText, tagName) {
1504
+ for (const [el] of this.managedElements) {
1505
+ if (el.tagName !== tagName) continue;
1506
+ const current = this.getElementContent(el);
1507
+ if (current === originalText) return el;
1508
+ }
1509
+ return null;
1029
1510
  }
1030
1511
  updateElementTexts() {
1031
1512
  this.observer?.disconnect();
1513
+ let orphanedCount = 0;
1032
1514
  for (const [el, info] of this.managedElements) {
1033
1515
  if (document.activeElement === el) continue;
1034
- const val = this.getPageText(info.key);
1516
+ const entry = this.pageTexts[info.key];
1517
+ if (!entry) continue;
1518
+ const val = entry[this.currentLang];
1035
1519
  if (!val) continue;
1036
- const current = this.hasEditableChildren(el) ? this.getDirectTextContent(el).trim() : el.textContent?.trim() || "";
1037
- if (val !== current) this.setDirectTextContent(el, val);
1520
+ const orig = entry[`_orig_${this.currentLang}`];
1521
+ if (!orig) {
1522
+ const current = this.getElementContent(el);
1523
+ if (val !== current) this.setElementContent(el, val);
1524
+ continue;
1525
+ }
1526
+ const domText = this.domOriginals[info.key] || this.getElementContent(el);
1527
+ if (domText === orig) {
1528
+ if (val !== this.getElementContent(el)) this.setElementContent(el, val);
1529
+ } else {
1530
+ orphanedCount++;
1531
+ }
1038
1532
  }
1039
1533
  this.observer?.observe(document.body, { childList: true, subtree: true });
1040
1534
  }
@@ -1043,6 +1537,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1043
1537
  this.managedElements.clear();
1044
1538
  for (const el of this.autoDetectedElements) el.removeAttribute("data-cms");
1045
1539
  this.autoDetectedElements.clear();
1540
+ this.domOriginals = {};
1046
1541
  this.cleanupManagedImages();
1047
1542
  }
1048
1543
  // ===============================================
@@ -1062,7 +1557,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1062
1557
  const info = this.managedImages.get(img);
1063
1558
  if (info) {
1064
1559
  img.removeEventListener("contextmenu", info.ctxHandler);
1065
- img.style.outline = "";
1560
+ img.style.removeProperty("outline");
1066
1561
  img.style.cursor = "";
1067
1562
  this.managedImages.delete(img);
1068
1563
  }
@@ -1082,14 +1577,14 @@ var XtroedgeCMS = class _XtroedgeCMS {
1082
1577
  enableImageEdit(img, ctxHandler) {
1083
1578
  if (!img.dataset.origTitle) img.dataset.origTitle = img.title || "";
1084
1579
  img.title = "Right-click to change image";
1085
- img.style.outline = `2px dashed ${this.highlightColor}`;
1580
+ img.style.setProperty("outline", `2px dashed ${this.highlightColor}`, "important");
1086
1581
  img.style.cursor = "context-menu";
1087
1582
  img.addEventListener("contextmenu", ctxHandler);
1088
1583
  }
1089
1584
  disableImageEdit(img, ctxHandler) {
1090
1585
  img.title = img.dataset.origTitle || "";
1091
1586
  delete img.dataset.origTitle;
1092
- img.style.outline = "";
1587
+ img.style.removeProperty("outline");
1093
1588
  img.style.cursor = "";
1094
1589
  img.removeEventListener("contextmenu", ctxHandler);
1095
1590
  }
@@ -1138,7 +1633,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1138
1633
  this.dismissImageCtxMenu();
1139
1634
  });
1140
1635
  menu.appendChild(btn);
1141
- document.body.appendChild(menu);
1636
+ (this.rootEl || document.body).appendChild(menu);
1142
1637
  this.imageCtxMenu = menu;
1143
1638
  const closeHandler = (e) => {
1144
1639
  if (!menu.contains(e.target)) {
@@ -1169,14 +1664,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1169
1664
  }
1170
1665
  async uploadImageToApi(dataUrl) {
1171
1666
  try {
1172
- const res = await this.apiFetch(`${this.config.apiBase}/web/upload-image`, {
1667
+ const storedImageUrl = _XtroedgeCMS.secureGet("xtroedge_image_url");
1668
+ const clientBase = this.config.clientApi || this.config.apiBase;
1669
+ const uploadUrl = storedImageUrl ? this.resolveClientUrl(storedImageUrl) : `${clientBase}/web/upload-image`;
1670
+ const res = await this.apiFetch(uploadUrl, {
1173
1671
  method: "POST",
1174
1672
  headers: { "Content-Type": "application/json" },
1175
1673
  body: JSON.stringify({ url: dataUrl })
1176
1674
  });
1177
1675
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
1178
1676
  const data = await res.json();
1179
- const baseUrl = this.config.imageBaseUrl || (this.config.apiBase ? new URL(this.config.apiBase).origin : "");
1677
+ const imgOriginBase = this.config.clientApi || this.config.apiBase || "";
1678
+ const baseUrl = this.config.imageBaseUrl || (imgOriginBase ? new URL(imgOriginBase).origin : "");
1180
1679
  const fullUrl = baseUrl + data.path;
1181
1680
  if (this.activeImageEl) {
1182
1681
  const key = this.managedImages.get(this.activeImageEl)?.key || "";
@@ -1213,6 +1712,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
1213
1712
  this.canUndo = true;
1214
1713
  this.canRedo = false;
1215
1714
  if (!this.pageTexts[key]) this.pageTexts[key] = {};
1715
+ const origKey = `_orig_${this.currentLang}`;
1716
+ if (!this.pageTexts[key][origKey]) {
1717
+ this.pageTexts[key][origKey] = this.domOriginals[key] || "";
1718
+ }
1216
1719
  this.pageTexts[key] = { ...this.pageTexts[key], [this.currentLang]: newValue };
1217
1720
  this.dirtyKeys.add(key);
1218
1721
  this.unsavedChanges = this.dirtyKeys.size + this.dirtyImageKeys.size;
@@ -1243,6 +1746,162 @@ var XtroedgeCMS = class _XtroedgeCMS {
1243
1746
  }
1244
1747
  }
1245
1748
  // ===============================================
1749
+ // RICH TEXT TOOLBAR
1750
+ // ===============================================
1751
+ get richTextEnabled() {
1752
+ return this.config.richText !== false;
1753
+ }
1754
+ sanitizeHTML(html) {
1755
+ const doc = new DOMParser().parseFromString(html, "text/html");
1756
+ const clean = (node) => {
1757
+ const children = Array.from(node.childNodes);
1758
+ for (const child of children) {
1759
+ if (child.nodeType === Node.TEXT_NODE) continue;
1760
+ if (child.nodeType === Node.ELEMENT_NODE) {
1761
+ const el = child;
1762
+ if (!_XtroedgeCMS.ALLOWED_TAGS.has(el.tagName)) {
1763
+ while (el.firstChild) el.parentNode?.insertBefore(el.firstChild, el);
1764
+ el.remove();
1765
+ } else {
1766
+ const allowedAttrs = _XtroedgeCMS.ALLOWED_ATTRS[el.tagName] || /* @__PURE__ */ new Set();
1767
+ for (const attr of Array.from(el.attributes)) {
1768
+ if (!allowedAttrs.has(attr.name)) el.removeAttribute(attr.name);
1769
+ }
1770
+ if (el.tagName === "A") {
1771
+ const href = el.getAttribute("href") || "";
1772
+ if (href.trim().toLowerCase().startsWith("javascript:")) {
1773
+ el.setAttribute("href", "#");
1774
+ }
1775
+ }
1776
+ clean(el);
1777
+ }
1778
+ } else {
1779
+ child.remove();
1780
+ }
1781
+ }
1782
+ };
1783
+ clean(doc.body);
1784
+ return doc.body.innerHTML;
1785
+ }
1786
+ createRichToolbar() {
1787
+ if (!this.richTextEnabled || this.richToolbarEl) return;
1788
+ const toolbar = document.createElement("div");
1789
+ toolbar.className = "lcms-rich-toolbar";
1790
+ const buttons = [
1791
+ { cmd: "bold", label: "B", title: "Bold" },
1792
+ { cmd: "italic", label: "<i>I</i>", title: "Italic" },
1793
+ { cmd: "underline", label: "<u>U</u>", title: "Underline" },
1794
+ { cmd: "strikeThrough", label: "<s>S</s>", title: "Strikethrough" }
1795
+ ];
1796
+ const linkButtons = [
1797
+ { cmd: "createLink", label: "\u{1F517}", title: "Insert Link" },
1798
+ { cmd: "unlink", label: "\u2298", title: "Remove Link" }
1799
+ ];
1800
+ const utilButtons = [
1801
+ { cmd: "removeFormat", label: "\u2715", title: "Clear Formatting" }
1802
+ ];
1803
+ const makeBtn = (item) => {
1804
+ const btn = document.createElement("button");
1805
+ btn.innerHTML = item.label;
1806
+ btn.title = item.title;
1807
+ btn.dataset.cmd = item.cmd;
1808
+ btn.addEventListener("mousedown", (e) => {
1809
+ e.preventDefault();
1810
+ this.execToolbarCommand(item.cmd);
1811
+ });
1812
+ return btn;
1813
+ };
1814
+ const addSep = () => {
1815
+ const sep = document.createElement("div");
1816
+ sep.className = "lcms-tb-sep";
1817
+ toolbar.appendChild(sep);
1818
+ };
1819
+ buttons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1820
+ addSep();
1821
+ linkButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1822
+ addSep();
1823
+ utilButtons.forEach((b) => toolbar.appendChild(makeBtn(b)));
1824
+ this.rootEl?.appendChild(toolbar);
1825
+ this.richToolbarEl = toolbar;
1826
+ this.selectionChangeHandler = () => this.updateToolbarState();
1827
+ document.addEventListener("selectionchange", this.selectionChangeHandler);
1828
+ }
1829
+ destroyRichToolbar() {
1830
+ if (this.richToolbarEl) {
1831
+ this.richToolbarEl.remove();
1832
+ this.richToolbarEl = null;
1833
+ }
1834
+ if (this.selectionChangeHandler) {
1835
+ document.removeEventListener("selectionchange", this.selectionChangeHandler);
1836
+ this.selectionChangeHandler = null;
1837
+ }
1838
+ this.activeEditableEl = null;
1839
+ }
1840
+ execToolbarCommand(cmd) {
1841
+ this.observer?.disconnect();
1842
+ if (this.scanTimeout) {
1843
+ clearTimeout(this.scanTimeout);
1844
+ this.scanTimeout = null;
1845
+ }
1846
+ if (cmd === "createLink") {
1847
+ const url = prompt("Enter URL:", "https://");
1848
+ if (url) document.execCommand("createLink", false, url);
1849
+ } else {
1850
+ document.execCommand(cmd, false);
1851
+ }
1852
+ this.updateToolbarState();
1853
+ if (this.activeEditableEl) {
1854
+ this.activeEditableEl.dispatchEvent(new Event("input", { bubbles: true }));
1855
+ }
1856
+ setTimeout(() => {
1857
+ this.observer?.observe(document.body, { childList: true, subtree: true });
1858
+ }, 50);
1859
+ }
1860
+ showRichToolbar(el) {
1861
+ if (!this.richTextEnabled || !this.richToolbarEl) return;
1862
+ if (this.toolbarHideTimeout) {
1863
+ clearTimeout(this.toolbarHideTimeout);
1864
+ this.toolbarHideTimeout = null;
1865
+ }
1866
+ this.activeEditableEl = el;
1867
+ this.positionToolbar(el);
1868
+ this.richToolbarEl.classList.add("visible");
1869
+ this.updateToolbarState();
1870
+ }
1871
+ positionToolbar(el) {
1872
+ if (!this.richToolbarEl) return;
1873
+ const rect = el.getBoundingClientRect();
1874
+ const tbRect = this.richToolbarEl.getBoundingClientRect();
1875
+ let top = rect.top - tbRect.height - 8;
1876
+ let left = rect.left + rect.width / 2 - tbRect.width / 2;
1877
+ if (top < 4) top = rect.bottom + 8;
1878
+ if (left < 4) left = 4;
1879
+ if (left + tbRect.width > window.innerWidth - 4) left = window.innerWidth - tbRect.width - 4;
1880
+ this.richToolbarEl.style.top = `${top}px`;
1881
+ this.richToolbarEl.style.left = `${left}px`;
1882
+ }
1883
+ hideRichToolbar() {
1884
+ if (!this.richToolbarEl) return;
1885
+ this.toolbarHideTimeout = setTimeout(() => {
1886
+ this.richToolbarEl?.classList.remove("visible");
1887
+ this.activeEditableEl = null;
1888
+ this.toolbarHideTimeout = null;
1889
+ }, 150);
1890
+ }
1891
+ updateToolbarState() {
1892
+ if (!this.richToolbarEl) return;
1893
+ const cmds = ["bold", "italic", "underline", "strikeThrough"];
1894
+ for (const btn of Array.from(this.richToolbarEl.querySelectorAll("button"))) {
1895
+ const cmd = btn.dataset.cmd;
1896
+ if (cmd && cmds.includes(cmd)) {
1897
+ try {
1898
+ btn.classList.toggle("active", document.queryCommandState(cmd));
1899
+ } catch {
1900
+ }
1901
+ }
1902
+ }
1903
+ }
1904
+ // ===============================================
1246
1905
  // UNDO / REDO
1247
1906
  // ===============================================
1248
1907
  onUndo() {
@@ -1304,7 +1963,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1304
1963
  this.updateUI();
1305
1964
  }
1306
1965
  logout() {
1307
- localStorage.removeItem("builder_token");
1966
+ _XtroedgeCMS.secureClear();
1308
1967
  sessionStorage.removeItem("builder_edit_mode");
1309
1968
  this.editMode = false;
1310
1969
  this.isEditAllowed = false;
@@ -1330,6 +1989,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
1330
1989
  }
1331
1990
  }
1332
1991
  this.autoDetectAndScan();
1992
+ this.captureDomOriginals();
1333
1993
  if (this.editMode) this.applyEditMode(true);
1334
1994
  if (this.config.apiBase) {
1335
1995
  if (loadDraft) this.loadPageContent("draft");
@@ -1361,17 +2021,40 @@ var XtroedgeCMS = class _XtroedgeCMS {
1361
2021
  getPageSection() {
1362
2022
  return this.currentSlug.replace(/^\//, "").replace(/-/g, "_").toUpperCase();
1363
2023
  }
1364
- // Authenticated fetch adds Authorization header from builder_token
2024
+ /** Resolve a stored URL against our server (apiBase) */
2025
+ resolveServerUrl(storedUrl) {
2026
+ if (storedUrl.startsWith("/")) return `${this.config.apiBase}${storedUrl}`;
2027
+ return storedUrl;
2028
+ }
2029
+ /** Resolve a stored URL against client's server (clientApi, falls back to apiBase) */
2030
+ resolveClientUrl(storedUrl) {
2031
+ const base = this.config.clientApi || this.config.apiBase;
2032
+ if (storedUrl.startsWith("/")) return `${base}${storedUrl}`;
2033
+ return storedUrl;
2034
+ }
1365
2035
  apiFetch(url, opts) {
1366
- const token = localStorage.getItem("builder_token") || "";
2036
+ const token = _XtroedgeCMS.secureGet("builder_token") || "";
2037
+ const licenseKey = this.config.licenseKey || "";
1367
2038
  const headers = { ...opts?.headers || {} };
1368
2039
  if (token) headers["Authorization"] = `Bearer ${token}`;
2040
+ if (licenseKey) headers["X-License-Key"] = licenseKey;
2041
+ const isOurServer = this.config.apiBase && url.startsWith(this.config.apiBase);
2042
+ if (!isOurServer && this.config.clientHeaders) {
2043
+ for (const [k, v] of Object.entries(this.config.clientHeaders)) {
2044
+ if (!headers[k]) headers[k] = v;
2045
+ }
2046
+ }
1369
2047
  return fetch(url, { ...opts, headers });
1370
2048
  }
1371
- // Fetch texts for a single slug and merge into pageTexts
2049
+ // Fetch page by slug if get_url exists in localStorage, call it directly (bypass our server)
1372
2050
  async fetchSection(slug, status) {
1373
2051
  try {
1374
- const res = await this.apiFetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(slug)}&status=${status}&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
2052
+ const storedGetUrl = _XtroedgeCMS.secureGet("xtroedge_get_url");
2053
+ const clientBase = this.config.clientApi || this.config.apiBase;
2054
+ const getBase = storedGetUrl ? this.resolveClientUrl(storedGetUrl) : `${clientBase}/web-page/get`;
2055
+ let getUrl = `${getBase}?slug=${encodeURIComponent(slug)}`;
2056
+ if (status) getUrl += `&status=${encodeURIComponent(status)}`;
2057
+ const res = await this.apiFetch(getUrl);
1375
2058
  if (!res.ok) return null;
1376
2059
  return await res.json();
1377
2060
  } catch {
@@ -1458,7 +2141,8 @@ var XtroedgeCMS = class _XtroedgeCMS {
1458
2141
  for (const key of this.registeredKeys) {
1459
2142
  const el = document.querySelector(`[data-cms="${key}"]`);
1460
2143
  const val = el?.textContent?.trim() || "";
1461
- texts[key] = { [this.currentLang]: val };
2144
+ const entry = { [this.defaultLanguage]: val };
2145
+ texts[key] = entry;
1462
2146
  }
1463
2147
  this.pageTexts = texts;
1464
2148
  if (this.config.i18nBasePath) {
@@ -1497,12 +2181,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1497
2181
  return sections;
1498
2182
  }
1499
2183
  async saveChanges() {
2184
+ if (!this.licenseValid) {
2185
+ this.showLicenseExpiredToast();
2186
+ return;
2187
+ }
1500
2188
  if (!this.config.apiBase) {
1501
2189
  this.showToast("No API configured. Set apiBase to enable save.", "error");
1502
2190
  return;
1503
2191
  }
1504
2192
  this.isSaving = true;
1505
2193
  this.updateUI();
2194
+ const origKey = `_orig_${this.currentLang}`;
2195
+ for (const key of Object.keys(this.pageTexts)) {
2196
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
2197
+ this.pageTexts[key][origKey] = this.domOriginals[key];
2198
+ }
2199
+ }
1506
2200
  try {
1507
2201
  const sections = this.groupTextsBySection();
1508
2202
  const requests = Object.entries(sections).map(
@@ -1512,13 +2206,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1512
2206
  body: JSON.stringify({
1513
2207
  slug,
1514
2208
  title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1515
- site_identifier: this.siteIdentifier,
1516
2209
  content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1517
2210
  })
1518
2211
  })
1519
2212
  );
1520
- const results = await Promise.all(requests);
1521
- if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
2213
+ const responses = await Promise.all(requests);
2214
+ const saveResults = [];
2215
+ for (const res of responses) {
2216
+ const data = await res.json();
2217
+ saveResults.push({ ok: res.ok, status: res.status, data });
2218
+ }
2219
+ if (saveResults.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
2220
+ console.info("[XtroEdge] Save responses:", saveResults.map((r) => r.data));
1522
2221
  this.isSaving = false;
1523
2222
  this.resetAfterSave();
1524
2223
  this.updateUI();
@@ -1532,12 +2231,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1532
2231
  }
1533
2232
  }
1534
2233
  async publishChanges() {
2234
+ if (!this.licenseValid) {
2235
+ this.showLicenseExpiredToast();
2236
+ return;
2237
+ }
1535
2238
  if (!this.config.apiBase) {
1536
2239
  this.showToast("No API configured. Set apiBase to enable publish.", "error");
1537
2240
  return;
1538
2241
  }
1539
2242
  this.isPublishing = true;
1540
2243
  this.updateUI();
2244
+ const origKey = `_orig_${this.currentLang}`;
2245
+ for (const key of Object.keys(this.pageTexts)) {
2246
+ if (!this.pageTexts[key][origKey] && this.domOriginals[key]) {
2247
+ this.pageTexts[key][origKey] = this.domOriginals[key];
2248
+ }
2249
+ }
1541
2250
  try {
1542
2251
  const sections = this.groupTextsBySection();
1543
2252
  const requests = Object.entries(sections).map(
@@ -1547,13 +2256,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1547
2256
  body: JSON.stringify({
1548
2257
  slug,
1549
2258
  title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1550
- site_identifier: this.siteIdentifier,
1551
2259
  published_content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1552
2260
  })
1553
2261
  })
1554
2262
  );
1555
2263
  const results = await Promise.all(requests);
1556
- if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
2264
+ const publishResults = [];
2265
+ for (const res of results) {
2266
+ const data = await res.json();
2267
+ publishResults.push({ ok: res.ok, status: res.status, data });
2268
+ }
2269
+ if (publishResults.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
2270
+ console.info("[XtroEdge] Publish responses:", publishResults.map((r) => r.data));
1557
2271
  this.isPublishing = false;
1558
2272
  this.resetAfterSave();
1559
2273
  this.updateUI();
@@ -1715,6 +2429,11 @@ var XtroedgeCMS = class _XtroedgeCMS {
1715
2429
  toggleEditMode(e) {
1716
2430
  e.stopPropagation();
1717
2431
  const checked = e.target.checked;
2432
+ if (checked && !this.licenseValid) {
2433
+ e.target.checked = false;
2434
+ this.showLicenseExpiredToast();
2435
+ return;
2436
+ }
1718
2437
  if (checked) {
1719
2438
  sessionStorage.setItem("builder_edit_mode", "true");
1720
2439
  this.editMode = true;
@@ -1846,13 +2565,19 @@ var XtroedgeCMS = class _XtroedgeCMS {
1846
2565
  </div>
1847
2566
  <div class="lcms-login-field">
1848
2567
  <label class="lcms-login-label">Password</label>
1849
- <input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
2568
+ <div class="lcms-password-wrapper">
2569
+ <input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
2570
+ <button type="button" class="lcms-password-toggle" id="lcms-password-toggle" tabindex="-1">
2571
+ <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>
2572
+ <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>
2573
+ </button>
2574
+ </div>
1850
2575
  </div>
1851
2576
  <button class="lcms-login-btn" id="lcms-login-btn">Sign In</button>
1852
2577
  <div class="lcms-login-error" id="lcms-login-error"></div>
1853
2578
  </div>
1854
2579
  `;
1855
- document.body.appendChild(overlay);
2580
+ (this.rootEl || document.body).appendChild(overlay);
1856
2581
  this.loginModalEl = overlay;
1857
2582
  const btn = overlay.querySelector("#lcms-login-btn");
1858
2583
  const emailInput = overlay.querySelector("#lcms-login-email");
@@ -1862,6 +2587,16 @@ var XtroedgeCMS = class _XtroedgeCMS {
1862
2587
  passInput.addEventListener("keydown", (e) => {
1863
2588
  if (e.key === "Enter") doLogin();
1864
2589
  });
2590
+ const toggleBtn = overlay.querySelector("#lcms-password-toggle");
2591
+ if (toggleBtn) {
2592
+ toggleBtn.addEventListener("click", () => {
2593
+ const isPassword = passInput.type === "password";
2594
+ passInput.type = isPassword ? "text" : "password";
2595
+ toggleBtn.querySelector(".lcms-eye-open").classList.toggle("lcms-hidden", !isPassword);
2596
+ toggleBtn.querySelector(".lcms-eye-closed").classList.toggle("lcms-hidden", isPassword);
2597
+ passInput.focus();
2598
+ });
2599
+ }
1865
2600
  setTimeout(() => emailInput.focus(), 50);
1866
2601
  }
1867
2602
  hideLoginModal() {
@@ -1897,7 +2632,21 @@ var XtroedgeCMS = class _XtroedgeCMS {
1897
2632
  }
1898
2633
  const token = data?.token || data?.authToken || data?.auth_token || data?.data?.token;
1899
2634
  if (!token) throw new Error("Login successful but no token returned.");
1900
- localStorage.setItem("builder_token", token);
2635
+ _XtroedgeCMS.secureSet("builder_token", token);
2636
+ if (data?.get_url) _XtroedgeCMS.secureSet("xtroedge_get_url", data.get_url);
2637
+ else _XtroedgeCMS.secureRemove("xtroedge_get_url");
2638
+ if (data?.published_url) _XtroedgeCMS.secureSet("xtroedge_published_url", data.published_url);
2639
+ else _XtroedgeCMS.secureRemove("xtroedge_published_url");
2640
+ if (data?.image_url) _XtroedgeCMS.secureSet("xtroedge_image_url", data.image_url);
2641
+ else _XtroedgeCMS.secureRemove("xtroedge_image_url");
2642
+ if (data?.client_api) {
2643
+ _XtroedgeCMS.secureSet("xtroedge_client_api", data.client_api);
2644
+ this.config.clientApi = data.client_api;
2645
+ } else {
2646
+ _XtroedgeCMS.secureRemove("xtroedge_client_api");
2647
+ }
2648
+ if (data?.save_url) _XtroedgeCMS.secureSet("xtroedge_save_url", data.save_url);
2649
+ else _XtroedgeCMS.secureRemove("xtroedge_save_url");
1901
2650
  this.hideLoginModal();
1902
2651
  this.editMode = true;
1903
2652
  this.pendingEditMode = false;
@@ -1953,10 +2702,10 @@ var XtroedgeCMS = class _XtroedgeCMS {
1953
2702
  document.documentElement.style.setProperty("--lcms-primary-dark", this.getDarkerColor(color));
1954
2703
  if (this.editMode) {
1955
2704
  for (const [el] of this.managedElements) {
1956
- el.style.outline = `2px dashed ${color}`;
2705
+ el.style.setProperty("outline", `2px dashed ${color}`, "important");
1957
2706
  }
1958
2707
  for (const [img] of this.managedImages) {
1959
- img.style.outline = `2px dashed ${color}`;
2708
+ img.style.setProperty("outline", `2px dashed ${color}`, "important");
1960
2709
  }
1961
2710
  }
1962
2711
  const swatches = this.panelEl?.querySelectorAll(".lcms-theme-swatch");
@@ -1969,23 +2718,71 @@ var XtroedgeCMS = class _XtroedgeCMS {
1969
2718
  // STATIC CONVENIENCE
1970
2719
  // ===============================================
1971
2720
  /** Quick init - create and start CMS in one call */
1972
- static create(config) {
2721
+ static async create(config) {
1973
2722
  const cms = new _XtroedgeCMS(config);
1974
- cms.init();
2723
+ await cms.init();
1975
2724
  return cms;
1976
2725
  }
1977
2726
  /** Auto-init: called automatically when package is loaded. No user code needed. */
1978
- static autoInit() {
2727
+ static async autoInit() {
1979
2728
  if (window.__xtroedge_cms_instance__) return;
1980
2729
  const userConfig = window.__XTROEDGE_CMS_CONFIG__ || {};
1981
- const cms = _XtroedgeCMS.create(userConfig);
2730
+ const cms = await _XtroedgeCMS.create(userConfig);
1982
2731
  window.__xtroedge_cms_instance__ = cms;
1983
2732
  }
1984
2733
  };
2734
+ // ===== Encrypted vault (single key stores all sensitive data) =====
2735
+ _XtroedgeCMS._SK = "xTr0EdG3_s3cUr3_k3y!@#2024";
2736
+ _XtroedgeCMS._VAULT_KEY = "_xtd";
2737
+ // ===============================================
2738
+ // LICENSE VALIDATION
2739
+ // ===============================================
2740
+ _XtroedgeCMS.LICENSE_CACHE_KEY = "xtroedge_license_cache";
2741
+ _XtroedgeCMS.LICENSE_CACHE_TTL = 24 * 60 * 60 * 1e3;
2742
+ // Tags where Enter should be blocked (single-line elements)
2743
+ _XtroedgeCMS.SINGLE_LINE_TAGS = /* @__PURE__ */ new Set([
2744
+ "H1",
2745
+ "H2",
2746
+ "H3",
2747
+ "H4",
2748
+ "H5",
2749
+ "H6",
2750
+ "BUTTON",
2751
+ "LABEL",
2752
+ "SPAN",
2753
+ "A",
2754
+ "SMALL",
2755
+ "B",
2756
+ "STRONG",
2757
+ "I",
2758
+ "EM"
2759
+ ]);
2760
+ // Allowed HTML tags for sanitization
2761
+ _XtroedgeCMS.ALLOWED_TAGS = /* @__PURE__ */ new Set([
2762
+ "B",
2763
+ "STRONG",
2764
+ "I",
2765
+ "EM",
2766
+ "U",
2767
+ "S",
2768
+ "STRIKE",
2769
+ "A",
2770
+ "BR",
2771
+ "UL",
2772
+ "OL",
2773
+ "LI",
2774
+ "SUB",
2775
+ "SUP"
2776
+ ]);
2777
+ // Allowed attributes per tag
2778
+ _XtroedgeCMS.ALLOWED_ATTRS = {
2779
+ A: /* @__PURE__ */ new Set(["href", "target", "rel"])
2780
+ };
2781
+ var XtroedgeCMS = _XtroedgeCMS;
1985
2782
 
1986
2783
  // src/index.ts
1987
- function boot() {
1988
- XtroedgeCMS.autoInit();
2784
+ async function boot() {
2785
+ await XtroedgeCMS.autoInit();
1989
2786
  }
1990
2787
  if (typeof window !== "undefined") {
1991
2788
  if (document.readyState === "loading") {