ngx-xtroedge-cms 1.3.1 → 1.3.3

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
@@ -237,6 +237,58 @@ var CMS_STYLES = `
237
237
  }
238
238
  .lcms-spinner-lg { width: 36px; height: 36px; border: 3px solid rgba(255,255,255,0.2); border-top-color: var(--lcms-primary, #00C853); border-radius: 50%; animation: lcmsSpin 0.6s linear infinite; }
239
239
 
240
+ /* LOGIN MODAL */
241
+ .lcms-login-overlay {
242
+ position: fixed; inset: 0; z-index: 10010;
243
+ display: flex; align-items: center; justify-content: center;
244
+ background: rgba(8, 8, 15, 0.75);
245
+ backdrop-filter: blur(16px) saturate(1.4);
246
+ -webkit-backdrop-filter: blur(16px) saturate(1.4);
247
+ font-family: system-ui, -apple-system, sans-serif;
248
+ }
249
+ .lcms-login-box {
250
+ background: #13151a; border: 1px solid rgba(255,255,255,0.1);
251
+ border-radius: 16px; padding: 36px 32px; width: 340px;
252
+ box-shadow: 0 24px 60px rgba(0,0,0,0.6);
253
+ }
254
+ .lcms-login-logo {
255
+ display: flex; align-items: center; justify-content: center;
256
+ gap: 8px; margin-bottom: 24px;
257
+ }
258
+ .lcms-login-logo-icon {
259
+ width: 32px; height: 32px; border-radius: 8px;
260
+ background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
261
+ display: flex; align-items: center; justify-content: center;
262
+ }
263
+ .lcms-login-logo-text { color: white; font-size: 15px; font-weight: 700; letter-spacing: 0.5px; }
264
+ .lcms-login-title { color: white; font-size: 18px; font-weight: 700; text-align: center; margin-bottom: 6px; }
265
+ .lcms-login-sub { color: rgba(255,255,255,0.4); font-size: 12px; text-align: center; margin-bottom: 24px; }
266
+ .lcms-login-field { margin-bottom: 14px; }
267
+ .lcms-login-label { display: block; color: rgba(255,255,255,0.6); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 6px; }
268
+ .lcms-login-input {
269
+ width: 100%; box-sizing: border-box;
270
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
271
+ border-radius: 8px; padding: 10px 12px;
272
+ color: white; font-size: 13px; font-family: inherit; outline: none;
273
+ transition: border-color 0.2s;
274
+ }
275
+ .lcms-login-input:focus { border-color: var(--lcms-primary, #00C853); }
276
+ .lcms-login-input::placeholder { color: rgba(255,255,255,0.25); }
277
+ .lcms-login-btn {
278
+ width: 100%; padding: 11px; margin-top: 6px; border: none; border-radius: 8px; cursor: pointer;
279
+ background: linear-gradient(135deg, var(--lcms-primary, #00C853), var(--lcms-primary-dark, #2E7D32));
280
+ color: white; font-size: 13px; font-weight: 700; font-family: inherit;
281
+ letter-spacing: 0.3px; transition: filter 0.2s;
282
+ }
283
+ .lcms-login-btn:hover { filter: brightness(1.1); }
284
+ .lcms-login-btn:disabled { opacity: 0.6; cursor: not-allowed; }
285
+ .lcms-login-error {
286
+ background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3);
287
+ border-radius: 8px; padding: 9px 12px; color: #f87171;
288
+ font-size: 12px; text-align: center; margin-top: 12px; display: none;
289
+ }
290
+ .lcms-login-error.visible { display: block; }
291
+
240
292
  /* HIDDEN ELEMENTS */
241
293
  .lcms-hidden { display: none !important; }
242
294
  `;
@@ -286,6 +338,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
286
338
  this.brandingEl = null;
287
339
  this.siteIdentifier = "";
288
340
  this.siteIdEl = null;
341
+ this.loginModalEl = null;
342
+ this.pendingEditMode = false;
343
+ // edit=true was requested but token missing
289
344
  // ===== State =====
290
345
  this.editMode = false;
291
346
  this.currentLang = "en";
@@ -475,12 +530,24 @@ var XtroedgeCMS = class _XtroedgeCMS {
475
530
  this.resetAll();
476
531
  }
477
532
  const editFromSession = sessionStorage.getItem("builder_edit_mode") === "true";
478
- if (editViaParam || editFromSession) {
533
+ const wantsEdit = editViaParam || editFromSession;
534
+ if (wantsEdit) {
535
+ const token = localStorage.getItem("builder_token");
536
+ if (!token) {
537
+ this.pendingEditMode = true;
538
+ this.currentLang = this.detectCurrentLanguage();
539
+ this.setLoading(true);
540
+ setTimeout(() => {
541
+ this.loadTranslationsAndInit(false);
542
+ this.showLoginModal();
543
+ }, 300);
544
+ return;
545
+ }
479
546
  this.editMode = true;
480
547
  }
481
548
  this.currentLang = this.detectCurrentLanguage();
482
549
  this.setLoading(true);
483
- const loadDraft = editViaParam || editFromSession;
550
+ const loadDraft = wantsEdit;
484
551
  setTimeout(() => {
485
552
  this.loadTranslationsAndInit(loadDraft);
486
553
  }, 300);
@@ -753,7 +820,7 @@ var XtroedgeCMS = class _XtroedgeCMS {
753
820
  if (this.containerSelector) {
754
821
  return document.querySelector(this.containerSelector) || document.body;
755
822
  }
756
- return document.querySelector(".layout") || document.body;
823
+ return document.body;
757
824
  }
758
825
  autoDetectElements() {
759
826
  const container = this.resolveContainer();
@@ -761,7 +828,29 @@ var XtroedgeCMS = class _XtroedgeCMS {
761
828
  for (const el of this.autoDetectedElements) {
762
829
  if (!document.contains(el)) this.autoDetectedElements.delete(el);
763
830
  }
831
+ const getSectionSlug = (el) => {
832
+ let parent = el.parentElement;
833
+ while (parent && parent !== document.body) {
834
+ const tag = parent.tagName.toLowerCase();
835
+ if (tag === "header" || tag.endsWith("-header")) return "/header";
836
+ if (tag === "footer" || tag.endsWith("-footer")) return "/footer";
837
+ parent = parent.parentElement;
838
+ }
839
+ return this.currentSlug;
840
+ };
764
841
  const tagCounters = {};
842
+ const assignKey = (el, sectionSlug) => {
843
+ const sectionKey = sectionSlug === "/header" ? "header" : sectionSlug === "/footer" ? "footer" : "page";
844
+ if (!tagCounters[sectionKey]) tagCounters[sectionKey] = {};
845
+ const tag = el.tagName.toLowerCase();
846
+ if (!tagCounters[sectionKey][tag]) tagCounters[sectionKey][tag] = 0;
847
+ const prefix = sectionKey !== "page" ? `${sectionKey}_` : "";
848
+ const key = `${prefix}xcms_${tag}_${tagCounters[sectionKey][tag]}`;
849
+ tagCounters[sectionKey][tag]++;
850
+ el.setAttribute("data-cms", key);
851
+ el.setAttribute("data-cms-section", sectionSlug);
852
+ this.autoDetectedElements.add(el);
853
+ };
765
854
  const selector = this.editableTags.join(",");
766
855
  const elements = container.querySelectorAll(selector);
767
856
  elements.forEach((el) => {
@@ -770,12 +859,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
770
859
  if (el.children.length > 3) return;
771
860
  const text = this.getDirectTextContent(el).trim();
772
861
  if (text.length < 2) return;
773
- const tag = el.tagName.toLowerCase();
774
- if (!tagCounters[tag]) tagCounters[tag] = 0;
775
- const key = `xcms_${tag}_${tagCounters[tag]}`;
776
- tagCounters[tag]++;
777
- el.setAttribute("data-cms", key);
778
- this.autoDetectedElements.add(el);
862
+ assignKey(el, getSectionSlug(el));
863
+ });
864
+ const dataEditables = container.querySelectorAll("[data-editable]");
865
+ dataEditables.forEach((el) => {
866
+ if (el.hasAttribute("data-cms")) return;
867
+ if (el.closest("#xtroedge-cms-root, script, style, noscript")) return;
868
+ assignKey(el, getSectionSlug(el));
779
869
  });
780
870
  }
781
871
  scanDOM() {
@@ -846,8 +936,13 @@ var XtroedgeCMS = class _XtroedgeCMS {
846
936
  const currentVal = this.getPageText(key);
847
937
  if (text !== currentVal) this.onTextChanged(key, text);
848
938
  };
849
- this.managedElements.set(el, { key, blurHandler, keydownHandler, inputHandler });
850
- if (this.editMode) this.enableElementEdit(el, key, blurHandler, keydownHandler, inputHandler);
939
+ const clickHandler = (e) => {
940
+ e.preventDefault();
941
+ e.stopPropagation();
942
+ };
943
+ const sectionSlug = el.getAttribute("data-cms-section") || this.currentSlug;
944
+ this.managedElements.set(el, { key, sectionSlug, blurHandler, keydownHandler, inputHandler, clickHandler });
945
+ if (this.editMode) this.enableElementEdit(el, key, blurHandler, keydownHandler, inputHandler, clickHandler);
851
946
  }
852
947
  detachElement(el) {
853
948
  const info = this.managedElements.get(el);
@@ -855,11 +950,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
855
950
  el.removeEventListener("blur", info.blurHandler);
856
951
  el.removeEventListener("keydown", info.keydownHandler);
857
952
  el.removeEventListener("input", info.inputHandler);
953
+ el.removeEventListener("click", info.clickHandler, true);
858
954
  el.removeAttribute("contenteditable");
859
955
  this.managedElements.delete(el);
860
956
  }
861
957
  }
862
- enableElementEdit(el, _key, blurH, keyH, inputH) {
958
+ enableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
863
959
  const val = this.getPageText(_key);
864
960
  if (val) el.textContent = val;
865
961
  el.setAttribute("contenteditable", "true");
@@ -873,8 +969,9 @@ var XtroedgeCMS = class _XtroedgeCMS {
873
969
  el.addEventListener("blur", blurH);
874
970
  el.addEventListener("keydown", keyH);
875
971
  el.addEventListener("input", inputH);
972
+ el.addEventListener("click", clickH, true);
876
973
  }
877
- disableElementEdit(el, _key, blurH, keyH, inputH) {
974
+ disableElementEdit(el, _key, blurH, keyH, inputH, clickH) {
878
975
  el.removeAttribute("contenteditable");
879
976
  el.style.borderBottom = "";
880
977
  el.style.padding = "";
@@ -888,11 +985,12 @@ var XtroedgeCMS = class _XtroedgeCMS {
888
985
  el.removeEventListener("blur", blurH);
889
986
  el.removeEventListener("keydown", keyH);
890
987
  el.removeEventListener("input", inputH);
988
+ el.removeEventListener("click", clickH, true);
891
989
  }
892
990
  applyEditMode(editMode) {
893
991
  for (const [el, info] of this.managedElements) {
894
- if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler);
895
- else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler);
992
+ if (editMode) this.enableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
993
+ else this.disableElementEdit(el, info.key, info.blurHandler, info.keydownHandler, info.inputHandler, info.clickHandler);
896
994
  }
897
995
  this.applyImageEditMode(editMode);
898
996
  }
@@ -1214,23 +1312,43 @@ var XtroedgeCMS = class _XtroedgeCMS {
1214
1312
  getPageSection() {
1215
1313
  return this.currentSlug.replace(/^\//, "").replace(/-/g, "_").toUpperCase();
1216
1314
  }
1315
+ // Fetch texts for a single slug and merge into pageTexts
1316
+ async fetchSectionTexts(slug, status) {
1317
+ try {
1318
+ const res = await fetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(slug)}&status=${status}&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
1319
+ if (!res.ok) return null;
1320
+ const page = await res.json();
1321
+ return page?.published_content?.texts || page?.content?.texts || null;
1322
+ } catch {
1323
+ return null;
1324
+ }
1325
+ }
1217
1326
  async loadPageContent(status) {
1218
1327
  this.setLoading(true);
1219
1328
  this.cleanOldHistory();
1220
1329
  try {
1221
- const res = await fetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(this.currentSlug)}&status=${status}&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
1222
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1223
- const page = await res.json();
1224
- const texts = page?.published_content?.texts || page?.content?.texts;
1225
- if (texts) {
1226
- this.pageTexts = JSON.parse(JSON.stringify(texts));
1330
+ const [pageTexts, headerTexts, footerTexts] = await Promise.all([
1331
+ this.fetchSectionTexts(this.currentSlug, status),
1332
+ this.fetchSectionTexts("/header", status),
1333
+ this.fetchSectionTexts("/footer", status)
1334
+ ]);
1335
+ if (pageTexts || headerTexts || footerTexts) {
1336
+ this.pageTexts = {
1337
+ ...pageTexts || {},
1338
+ ...headerTexts || {},
1339
+ ...footerTexts || {}
1340
+ };
1227
1341
  } else {
1228
1342
  this.buildDefaultTexts();
1229
1343
  }
1230
- const images = page?.published_content?.images || page?.content?.images;
1231
- if (images) {
1232
- this.pageImages = JSON.parse(JSON.stringify(images));
1233
- this.applyPageImages();
1344
+ const resImg = await fetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(this.currentSlug)}&status=${status}&site_identifier=${encodeURIComponent(this.siteIdentifier)}`).catch(() => null);
1345
+ if (resImg?.ok) {
1346
+ const page = await resImg.json();
1347
+ const images = page?.published_content?.images || page?.content?.images;
1348
+ if (images) {
1349
+ this.pageImages = JSON.parse(JSON.stringify(images));
1350
+ this.applyPageImages();
1351
+ }
1234
1352
  }
1235
1353
  this.originalTexts = JSON.parse(JSON.stringify(this.pageTexts));
1236
1354
  this.originalImages = JSON.parse(JSON.stringify(this.pageImages));
@@ -1248,18 +1366,24 @@ var XtroedgeCMS = class _XtroedgeCMS {
1248
1366
  async loadPublishedContent() {
1249
1367
  this.setLoading(true);
1250
1368
  try {
1251
- const res = await fetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(this.currentSlug)}&status=published&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
1252
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1253
- const page = await res.json();
1254
- const texts = page?.published_content?.texts;
1255
- if (texts && Object.keys(texts).length > 0) {
1256
- this.pageTexts = JSON.parse(JSON.stringify(texts));
1369
+ const [pageTexts, headerTexts, footerTexts] = await Promise.all([
1370
+ this.fetchSectionTexts(this.currentSlug, "published"),
1371
+ this.fetchSectionTexts("/header", "published"),
1372
+ this.fetchSectionTexts("/footer", "published")
1373
+ ]);
1374
+ const merged = { ...pageTexts || {}, ...headerTexts || {}, ...footerTexts || {} };
1375
+ if (Object.keys(merged).length > 0) {
1376
+ this.pageTexts = JSON.parse(JSON.stringify(merged));
1257
1377
  this.updateElementTexts();
1258
1378
  }
1259
- const images = page?.published_content?.images;
1260
- if (images && Object.keys(images).length > 0) {
1261
- this.pageImages = JSON.parse(JSON.stringify(images));
1262
- this.applyPageImages();
1379
+ const res = await fetch(`${this.config.apiBase}/web-page/get?slug=${encodeURIComponent(this.currentSlug)}&status=published&site_identifier=${encodeURIComponent(this.siteIdentifier)}`);
1380
+ if (res.ok) {
1381
+ const page = await res.json();
1382
+ const images = page?.published_content?.images;
1383
+ if (images && Object.keys(images).length > 0) {
1384
+ this.pageImages = JSON.parse(JSON.stringify(images));
1385
+ this.applyPageImages();
1386
+ }
1263
1387
  }
1264
1388
  } catch {
1265
1389
  } finally {
@@ -1297,6 +1421,18 @@ var XtroedgeCMS = class _XtroedgeCMS {
1297
1421
  }
1298
1422
  }
1299
1423
  }
1424
+ // Group pageTexts by section slug based on managedElements info
1425
+ groupTextsBySection() {
1426
+ const sections = {};
1427
+ for (const [, info] of this.managedElements) {
1428
+ const slug = info.sectionSlug;
1429
+ if (!sections[slug]) sections[slug] = {};
1430
+ if (this.pageTexts[info.key] !== void 0) {
1431
+ sections[slug][info.key] = this.pageTexts[info.key];
1432
+ }
1433
+ }
1434
+ return sections;
1435
+ }
1300
1436
  async saveChanges() {
1301
1437
  if (!this.config.apiBase) {
1302
1438
  this.showToast("No API configured. Set apiBase to enable save.", "error");
@@ -1304,19 +1440,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1304
1440
  }
1305
1441
  this.isSaving = true;
1306
1442
  this.updateUI();
1307
- const payload = {
1308
- slug: this.currentSlug,
1309
- title: this.currentTitle,
1310
- site_identifier: this.siteIdentifier,
1311
- content: { texts: this.pageTexts, images: this.pageImages }
1312
- };
1313
1443
  try {
1314
- const res = await fetch(`${this.config.apiBase}/web_page/save`, {
1315
- method: "POST",
1316
- headers: { "Content-Type": "application/json" },
1317
- body: JSON.stringify(payload)
1318
- });
1319
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1444
+ const sections = this.groupTextsBySection();
1445
+ const requests = Object.entries(sections).map(
1446
+ ([slug, texts]) => fetch(`${this.config.apiBase}/web_page/save`, {
1447
+ method: "POST",
1448
+ headers: { "Content-Type": "application/json" },
1449
+ body: JSON.stringify({
1450
+ slug,
1451
+ title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1452
+ site_identifier: this.siteIdentifier,
1453
+ content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1454
+ })
1455
+ })
1456
+ );
1457
+ const results = await Promise.all(requests);
1458
+ if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to save");
1320
1459
  this.isSaving = false;
1321
1460
  this.resetAfterSave();
1322
1461
  this.updateUI();
@@ -1336,19 +1475,22 @@ var XtroedgeCMS = class _XtroedgeCMS {
1336
1475
  }
1337
1476
  this.isPublishing = true;
1338
1477
  this.updateUI();
1339
- const payload = {
1340
- slug: this.currentSlug,
1341
- title: this.currentTitle,
1342
- site_identifier: this.siteIdentifier,
1343
- published_content: { texts: this.pageTexts, images: this.pageImages }
1344
- };
1345
1478
  try {
1346
- const res = await fetch(`${this.config.apiBase}/web_page/publish`, {
1347
- method: "POST",
1348
- headers: { "Content-Type": "application/json" },
1349
- body: JSON.stringify(payload)
1350
- });
1351
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
1479
+ const sections = this.groupTextsBySection();
1480
+ const requests = Object.entries(sections).map(
1481
+ ([slug, texts]) => fetch(`${this.config.apiBase}/web_page/publish`, {
1482
+ method: "POST",
1483
+ headers: { "Content-Type": "application/json" },
1484
+ body: JSON.stringify({
1485
+ slug,
1486
+ title: slug === this.currentSlug ? this.currentTitle : slug.replace("/", ""),
1487
+ site_identifier: this.siteIdentifier,
1488
+ published_content: { texts, ...slug === this.currentSlug ? { images: this.pageImages } : {} }
1489
+ })
1490
+ })
1491
+ );
1492
+ const results = await Promise.all(requests);
1493
+ if (results.some((r) => !r.ok)) throw new Error("One or more sections failed to publish");
1352
1494
  this.isPublishing = false;
1353
1495
  this.resetAfterSave();
1354
1496
  this.updateUI();
@@ -1616,6 +1758,89 @@ var XtroedgeCMS = class _XtroedgeCMS {
1616
1758
  return `${months[d.getMonth()]} ${d.getDate()}, ${h12}:${m} ${ampm}`;
1617
1759
  }
1618
1760
  // ===============================================
1761
+ // LOGIN MODAL
1762
+ // ===============================================
1763
+ showLoginModal() {
1764
+ if (this.loginModalEl) return;
1765
+ const overlay = document.createElement("div");
1766
+ overlay.className = "lcms-login-overlay";
1767
+ overlay.id = "lcms-login-modal";
1768
+ overlay.innerHTML = `
1769
+ <div class="lcms-login-box">
1770
+ <div class="lcms-login-logo">
1771
+ <div class="lcms-login-logo-icon">
1772
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
1773
+ </div>
1774
+ <span class="lcms-login-logo-text">XtroEdge CMS</span>
1775
+ </div>
1776
+ <div class="lcms-login-title">Builder Login</div>
1777
+ <div class="lcms-login-sub">Sign in to access edit mode</div>
1778
+ <div class="lcms-login-field">
1779
+ <label class="lcms-login-label">Email or Username</label>
1780
+ <input class="lcms-login-input" id="lcms-login-email" type="text" placeholder="Enter your email or username" autocomplete="username" />
1781
+ </div>
1782
+ <div class="lcms-login-field">
1783
+ <label class="lcms-login-label">Password</label>
1784
+ <input class="lcms-login-input" id="lcms-login-password" type="password" placeholder="Enter your password" autocomplete="current-password" />
1785
+ </div>
1786
+ <button class="lcms-login-btn" id="lcms-login-btn">Sign In</button>
1787
+ <div class="lcms-login-error" id="lcms-login-error"></div>
1788
+ </div>
1789
+ `;
1790
+ document.body.appendChild(overlay);
1791
+ this.loginModalEl = overlay;
1792
+ const btn = overlay.querySelector("#lcms-login-btn");
1793
+ const emailInput = overlay.querySelector("#lcms-login-email");
1794
+ const passInput = overlay.querySelector("#lcms-login-password");
1795
+ const doLogin = () => this.attemptLogin(emailInput.value.trim(), passInput.value);
1796
+ btn.addEventListener("click", doLogin);
1797
+ passInput.addEventListener("keydown", (e) => {
1798
+ if (e.key === "Enter") doLogin();
1799
+ });
1800
+ setTimeout(() => emailInput.focus(), 50);
1801
+ }
1802
+ hideLoginModal() {
1803
+ this.loginModalEl?.remove();
1804
+ this.loginModalEl = null;
1805
+ }
1806
+ async attemptLogin(email, password) {
1807
+ const btn = document.getElementById("lcms-login-btn");
1808
+ const errorEl = document.getElementById("lcms-login-error");
1809
+ if (!email || !password) {
1810
+ errorEl.textContent = "Please enter your email/username and password.";
1811
+ errorEl.classList.add("visible");
1812
+ return;
1813
+ }
1814
+ btn.disabled = true;
1815
+ btn.textContent = "Signing in...";
1816
+ errorEl.classList.remove("visible");
1817
+ try {
1818
+ const loginUrl = this.config.loginUrl || `${this.config.apiBase}/auth/login`;
1819
+ const res = await fetch(loginUrl, {
1820
+ method: "POST",
1821
+ headers: { "Content-Type": "application/json" },
1822
+ body: JSON.stringify({ email, password })
1823
+ });
1824
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1825
+ const data = await res.json();
1826
+ const token = data?.token || data?.authToken || data?.auth_token || data?.data?.token;
1827
+ if (!token) throw new Error("No token in response");
1828
+ localStorage.setItem("builder_token", token);
1829
+ this.hideLoginModal();
1830
+ this.editMode = true;
1831
+ this.pendingEditMode = false;
1832
+ sessionStorage.setItem("builder_edit_mode", "true");
1833
+ this.applyEditMode(true);
1834
+ this.updateUI();
1835
+ this.loadPageContent("draft");
1836
+ } catch {
1837
+ btn.disabled = false;
1838
+ btn.textContent = "Sign In";
1839
+ errorEl.textContent = "Invalid credentials. Please try again.";
1840
+ errorEl.classList.add("visible");
1841
+ }
1842
+ }
1843
+ // ===============================================
1619
1844
  // SITE IDENTIFIER
1620
1845
  // ===============================================
1621
1846
  resolveSiteIdentifier() {