Theme editor
This commit is contained in:
		@@ -29,8 +29,8 @@ document.addEventListener("DOMContentLoaded", () => {
 | 
			
		||||
      div.style.gap = "8px";
 | 
			
		||||
      div.style.marginBottom = "6px";
 | 
			
		||||
      div.innerHTML = `
 | 
			
		||||
        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label">
 | 
			
		||||
        <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href">
 | 
			
		||||
        <input type="text" placeholder="Label" value="${item.label || ""}" style="flex:1;" data-idx="${idx}" data-type="label" required>
 | 
			
		||||
        <input type="text" placeholder="?tag=tag1,tag2" value="${item.href || ""}" style="flex:2;" data-idx="${idx}" data-type="href" required>
 | 
			
		||||
        <button type="button" class="remove-menu-item" data-idx="${idx}">🗑</button>
 | 
			
		||||
      `;
 | 
			
		||||
      menuList.appendChild(div);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										393
									
								
								src/webui/js/theme-editor.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								src/webui/js/theme-editor.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,393 @@
 | 
			
		||||
async function fetchThemeInfo() {
 | 
			
		||||
  const res = await fetch("/api/theme-info");
 | 
			
		||||
  return await res.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function fetchLocalFonts(theme) {
 | 
			
		||||
  const res = await fetch(`/api/local-fonts?theme=${encodeURIComponent(theme)}`);
 | 
			
		||||
  return await res.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function removeFont(theme, font) {
 | 
			
		||||
  const res = await fetch("/api/font/remove", {
 | 
			
		||||
    method: "POST",
 | 
			
		||||
    headers: { "Content-Type": "application/json" },
 | 
			
		||||
    body: JSON.stringify({ theme, font })
 | 
			
		||||
  });
 | 
			
		||||
  return await res.json();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showToast(message, type = "success", duration = 3000) {
 | 
			
		||||
  const container = document.getElementById("toast-container");
 | 
			
		||||
  if (!container) return;
 | 
			
		||||
  const toast = document.createElement("div");
 | 
			
		||||
  toast.className = `toast ${type}`;
 | 
			
		||||
  toast.textContent = message;
 | 
			
		||||
  container.appendChild(toast);
 | 
			
		||||
  requestAnimationFrame(() => toast.classList.add("show"));
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    toast.classList.remove("show");
 | 
			
		||||
    setTimeout(() => container.removeChild(toast), 300);
 | 
			
		||||
  }, duration);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setColorInput(colorId, textId, value) {
 | 
			
		||||
  const colorInput = document.getElementById(colorId);
 | 
			
		||||
  const textInput = document.getElementById(textId);
 | 
			
		||||
  if (colorInput) colorInput.value = value;
 | 
			
		||||
  if (textInput) textInput.value = value;
 | 
			
		||||
  if (colorInput && textInput) {
 | 
			
		||||
    colorInput.addEventListener("input", () => {
 | 
			
		||||
      textInput.value = colorInput.value;
 | 
			
		||||
    });
 | 
			
		||||
    textInput.addEventListener("input", () => {
 | 
			
		||||
      colorInput.value = textInput.value;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setFontDropdown(selectId, value, options) {
 | 
			
		||||
  const select = document.getElementById(selectId);
 | 
			
		||||
  if (!select) return;
 | 
			
		||||
  select.innerHTML = options.map(opt =>
 | 
			
		||||
    `<option value="${opt}"${opt === value ? " selected" : ""}>${opt}</option>`
 | 
			
		||||
  ).join("");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setFallbackDropdown(selectId, value) {
 | 
			
		||||
  const select = document.getElementById(selectId);
 | 
			
		||||
  if (!select) return;
 | 
			
		||||
  select.value = (value === "serif" || value === "sans-serif") ? value : "sans-serif";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function setTextInput(inputId, value) {
 | 
			
		||||
  const input = document.getElementById(inputId);
 | 
			
		||||
  if (input) input.value = value;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderGoogleFonts(googleFonts) {
 | 
			
		||||
  const container = document.getElementById("google-fonts-fields");
 | 
			
		||||
  container.innerHTML = "";
 | 
			
		||||
  googleFonts.forEach((font, idx) => {
 | 
			
		||||
    container.innerHTML += `
 | 
			
		||||
      <div class="input-field" data-idx="${idx}">
 | 
			
		||||
        <label>Family</label>
 | 
			
		||||
        <input type="text" name="google_fonts[${idx}][family]" value="${font.family || ""}">
 | 
			
		||||
        <label>Weights (comma separated)</label>
 | 
			
		||||
        <input type="text" name="google_fonts[${idx}][weights]" value="${(font.weights || []).join(',')}">
 | 
			
		||||
        <button type="button" class="remove-google-font" data-idx="${idx}">Remove</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderLocalFonts(fonts) {
 | 
			
		||||
  const listDiv = document.getElementById("local-fonts-list");
 | 
			
		||||
  if (!listDiv) return;
 | 
			
		||||
  listDiv.innerHTML = "";
 | 
			
		||||
  fonts.forEach(font => {
 | 
			
		||||
    listDiv.innerHTML += `
 | 
			
		||||
      <div class="font-item">
 | 
			
		||||
        <span>${font}</span>
 | 
			
		||||
        <button type="button" class="remove-font-btn danger" data-font="${font}">Remove</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", async () => {
 | 
			
		||||
  const themeInfo = await fetchThemeInfo();
 | 
			
		||||
  const themeNameSpan = document.getElementById("current-theme");
 | 
			
		||||
  if (themeNameSpan) themeNameSpan.textContent = themeInfo.theme_name;
 | 
			
		||||
 | 
			
		||||
  const themeYaml = themeInfo.theme_yaml;
 | 
			
		||||
  const googleFonts = themeYaml.google_fonts ? JSON.parse(JSON.stringify(themeYaml.google_fonts)) : [];
 | 
			
		||||
  let localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
			
		||||
 | 
			
		||||
  // Colors
 | 
			
		||||
  if (themeYaml.colors) {
 | 
			
		||||
    setColorInput("color-primary", "color-primary-text", themeYaml.colors.primary || "#0065a1");
 | 
			
		||||
    setColorInput("color-primary-dark", "color-primary-dark-text", themeYaml.colors.primary_dark || "#005384");
 | 
			
		||||
    setColorInput("color-secondary", "color-secondary-text", themeYaml.colors.secondary || "#00b0f0");
 | 
			
		||||
    setColorInput("color-accent", "color-accent-text", themeYaml.colors.accent || "#ffc700");
 | 
			
		||||
    setColorInput("color-text-dark", "color-text-dark-text", themeYaml.colors.text_dark || "#616161");
 | 
			
		||||
    setColorInput("color-background", "color-background-text", themeYaml.colors.background || "#fff");
 | 
			
		||||
    setColorInput("color-browser-color", "color-browser-color-text", themeYaml.colors.browser_color || "#fff");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Fonts
 | 
			
		||||
  function refreshFontDropdowns() {
 | 
			
		||||
    setFontDropdown("font-primary", document.getElementById("font-primary").value, [
 | 
			
		||||
      ...googleFonts.map(f => f.family),
 | 
			
		||||
      ...localFonts
 | 
			
		||||
    ]);
 | 
			
		||||
    setFontDropdown("font-secondary", document.getElementById("font-secondary").value, [
 | 
			
		||||
      ...googleFonts.map(f => f.family),
 | 
			
		||||
      ...localFonts
 | 
			
		||||
    ]);
 | 
			
		||||
  }
 | 
			
		||||
  if (themeYaml.fonts) {
 | 
			
		||||
    setFontDropdown("font-primary", themeYaml.fonts.primary?.name || "Lato", [
 | 
			
		||||
      ...googleFonts.map(f => f.family),
 | 
			
		||||
      ...localFonts
 | 
			
		||||
    ]);
 | 
			
		||||
    setFallbackDropdown("font-primary-fallback", themeYaml.fonts.primary?.fallback || "sans-serif");
 | 
			
		||||
    setFontDropdown("font-secondary", themeYaml.fonts.secondary?.name || "Montserrat", [
 | 
			
		||||
      ...googleFonts.map(f => f.family),
 | 
			
		||||
      ...localFonts
 | 
			
		||||
    ]);
 | 
			
		||||
    setFallbackDropdown("font-secondary-fallback", themeYaml.fonts.secondary?.fallback || "serif");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Font upload logic
 | 
			
		||||
  const fontUploadInput = document.getElementById("font-upload");
 | 
			
		||||
  const chooseFontBtn = document.getElementById("choose-font-btn");
 | 
			
		||||
  const fontUploadStatus = document.getElementById("font-upload-status");
 | 
			
		||||
  const localFontsList = document.getElementById("local-fonts-list");
 | 
			
		||||
 | 
			
		||||
  // Modal logic for font deletion
 | 
			
		||||
  const deleteFontModal = document.getElementById("delete-font-modal");
 | 
			
		||||
  const deleteFontModalClose = document.getElementById("delete-font-modal-close");
 | 
			
		||||
  const deleteFontModalConfirm = document.getElementById("delete-font-modal-confirm");
 | 
			
		||||
  const deleteFontModalCancel = document.getElementById("delete-font-modal-cancel");
 | 
			
		||||
  let fontToDelete = null;
 | 
			
		||||
 | 
			
		||||
  function refreshLocalFonts() {
 | 
			
		||||
    renderLocalFonts(localFonts);
 | 
			
		||||
    refreshFontDropdowns();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (chooseFontBtn && fontUploadInput) {
 | 
			
		||||
    chooseFontBtn.addEventListener("click", () => fontUploadInput.click());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (fontUploadInput) {
 | 
			
		||||
    fontUploadInput.addEventListener("change", async (e) => {
 | 
			
		||||
      const file = e.target.files[0];
 | 
			
		||||
      if (!file) return;
 | 
			
		||||
      const ext = file.name.split('.').pop().toLowerCase();
 | 
			
		||||
      if (!["woff", "woff2"].includes(ext)) {
 | 
			
		||||
        fontUploadStatus.textContent = "Only .woff and .woff2 fonts are allowed.";
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append("file", file);
 | 
			
		||||
      formData.append("theme", themeInfo.theme_name);
 | 
			
		||||
      const res = await fetch("/api/font/upload", { method: "POST", body: formData });
 | 
			
		||||
      const result = await res.json();
 | 
			
		||||
      if (result.status === "ok") {
 | 
			
		||||
        fontUploadStatus.textContent = "Font uploaded!";
 | 
			
		||||
        showToast("Font uploaded!", "success");
 | 
			
		||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
			
		||||
        refreshLocalFonts();
 | 
			
		||||
      } else {
 | 
			
		||||
        fontUploadStatus.textContent = "Error uploading font.";
 | 
			
		||||
        showToast("Error uploading font.", "error");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Remove font button triggers modal
 | 
			
		||||
  if (localFontsList) {
 | 
			
		||||
    localFontsList.addEventListener("click", (e) => {
 | 
			
		||||
      if (e.target.classList.contains("remove-font-btn")) {
 | 
			
		||||
        fontToDelete = e.target.dataset.font;
 | 
			
		||||
        document.getElementById("delete-font-modal-text").textContent =
 | 
			
		||||
          `Are you sure you want to remove the font "${fontToDelete}"?`;
 | 
			
		||||
        deleteFontModal.style.display = "flex";
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Modal logic for font deletion
 | 
			
		||||
  if (deleteFontModal && deleteFontModalClose && deleteFontModalConfirm && deleteFontModalCancel) {
 | 
			
		||||
    deleteFontModalClose.onclick = deleteFontModalCancel.onclick = () => {
 | 
			
		||||
      deleteFontModal.style.display = "none";
 | 
			
		||||
      fontToDelete = null;
 | 
			
		||||
    };
 | 
			
		||||
    window.onclick = function(event) {
 | 
			
		||||
      if (event.target === deleteFontModal) {
 | 
			
		||||
        deleteFontModal.style.display = "none";
 | 
			
		||||
        fontToDelete = null;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    deleteFontModalConfirm.onclick = async () => {
 | 
			
		||||
      if (!fontToDelete) return;
 | 
			
		||||
      const result = await removeFont(themeInfo.theme_name, fontToDelete);
 | 
			
		||||
      if (result.status === "ok") {
 | 
			
		||||
        showToast("Font removed!", "success");
 | 
			
		||||
        localFonts = await fetchLocalFonts(themeInfo.theme_name);
 | 
			
		||||
        refreshLocalFonts();
 | 
			
		||||
      } else {
 | 
			
		||||
        showToast("Error removing font.", "error");
 | 
			
		||||
      }
 | 
			
		||||
      deleteFontModal.style.display = "none";
 | 
			
		||||
      fontToDelete = null;
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initial render of local fonts
 | 
			
		||||
  refreshLocalFonts();
 | 
			
		||||
 | 
			
		||||
  // Favicon logic
 | 
			
		||||
  const faviconInput = document.getElementById("favicon-path");
 | 
			
		||||
  const faviconUpload = document.getElementById("favicon-upload");
 | 
			
		||||
  const chooseFaviconBtn = document.getElementById("choose-favicon-btn");
 | 
			
		||||
  const faviconPreview = document.getElementById("favicon-preview");
 | 
			
		||||
  const removeFaviconBtn = document.getElementById("remove-favicon-btn");
 | 
			
		||||
  const deleteFaviconModal = document.getElementById("delete-favicon-modal");
 | 
			
		||||
  const deleteFaviconModalClose = document.getElementById("delete-favicon-modal-close");
 | 
			
		||||
  const deleteFaviconModalConfirm = document.getElementById("delete-favicon-modal-confirm");
 | 
			
		||||
  const deleteFaviconModalCancel = document.getElementById("delete-favicon-modal-cancel");
 | 
			
		||||
 | 
			
		||||
  function updateFaviconPreview(src) {
 | 
			
		||||
    if (faviconPreview) {
 | 
			
		||||
      faviconPreview.src = src || "";
 | 
			
		||||
      faviconPreview.style.display = src ? "inline-block" : "none";
 | 
			
		||||
    }
 | 
			
		||||
    if (removeFaviconBtn) {
 | 
			
		||||
      removeFaviconBtn.style.display = src ? "inline-block" : "none";
 | 
			
		||||
    }
 | 
			
		||||
    if (chooseFaviconBtn) {
 | 
			
		||||
      chooseFaviconBtn.style.display = src ? "none" : "inline-block";
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (chooseFaviconBtn && faviconUpload) {
 | 
			
		||||
    chooseFaviconBtn.addEventListener("click", () => faviconUpload.click());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (faviconUpload) {
 | 
			
		||||
    faviconUpload.addEventListener("change", async (e) => {
 | 
			
		||||
      const file = e.target.files[0];
 | 
			
		||||
      if (!file) return;
 | 
			
		||||
      const ext = file.name.split('.').pop().toLowerCase();
 | 
			
		||||
      if (!["png", "jpg", "jpeg", "ico"].includes(ext)) {
 | 
			
		||||
        showToast("Invalid file type for favicon.", "error");
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      const formData = new FormData();
 | 
			
		||||
      formData.append("file", file);
 | 
			
		||||
      formData.append("theme", themeInfo.theme_name);
 | 
			
		||||
      const res = await fetch("/api/favicon/upload", { method: "POST", body: formData });
 | 
			
		||||
      const result = await res.json();
 | 
			
		||||
      if (result.status === "ok") {
 | 
			
		||||
        faviconInput.value = result.filename;
 | 
			
		||||
        updateFaviconPreview(`/themes/${themeInfo.theme_name}/${result.filename}?t=${Date.now()}`);
 | 
			
		||||
        showToast("Favicon uploaded!", "success");
 | 
			
		||||
      } else {
 | 
			
		||||
        showToast("Error uploading favicon", "error");
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (removeFaviconBtn) {
 | 
			
		||||
    removeFaviconBtn.addEventListener("click", () => {
 | 
			
		||||
      deleteFaviconModal.style.display = "flex";
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (deleteFaviconModal && deleteFaviconModalClose && deleteFaviconModalConfirm && deleteFaviconModalCancel) {
 | 
			
		||||
    deleteFaviconModalClose.onclick = deleteFaviconModalCancel.onclick = () => {
 | 
			
		||||
      deleteFaviconModal.style.display = "none";
 | 
			
		||||
    };
 | 
			
		||||
    window.onclick = function(event) {
 | 
			
		||||
      if (event.target === deleteFaviconModal) {
 | 
			
		||||
        deleteFaviconModal.style.display = "none";
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    deleteFaviconModalConfirm.onclick = async () => {
 | 
			
		||||
      const res = await fetch("/api/favicon/remove", {
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        headers: { "Content-Type": "application/json" },
 | 
			
		||||
        body: JSON.stringify({ theme: themeInfo.theme_name })
 | 
			
		||||
      });
 | 
			
		||||
      const result = await res.json();
 | 
			
		||||
      if (result.status === "ok") {
 | 
			
		||||
        faviconInput.value = "";
 | 
			
		||||
        updateFaviconPreview("");
 | 
			
		||||
        showToast("Favicon removed!", "success");
 | 
			
		||||
      } else {
 | 
			
		||||
        showToast("Error removing favicon", "error");
 | 
			
		||||
      }
 | 
			
		||||
      deleteFaviconModal.style.display = "none";
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (themeYaml.favicon && themeYaml.favicon.path) {
 | 
			
		||||
    faviconInput.value = themeYaml.favicon.path;
 | 
			
		||||
    updateFaviconPreview(`/themes/${themeInfo.theme_name}/${themeYaml.favicon.path}?t=${Date.now()}`);
 | 
			
		||||
  } else {
 | 
			
		||||
    updateFaviconPreview("");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Google Fonts
 | 
			
		||||
  renderGoogleFonts(googleFonts);
 | 
			
		||||
 | 
			
		||||
  // Add Google Font
 | 
			
		||||
  const addGoogleFontBtn = document.getElementById("add-google-font");
 | 
			
		||||
  if (addGoogleFontBtn) {
 | 
			
		||||
    addGoogleFontBtn.addEventListener("click", () => {
 | 
			
		||||
      googleFonts.push({ family: "", weights: [] });
 | 
			
		||||
      renderGoogleFonts(googleFonts);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Remove Google Font
 | 
			
		||||
  const googleFontsFields = document.getElementById("google-fonts-fields");
 | 
			
		||||
  if (googleFontsFields) {
 | 
			
		||||
    googleFontsFields.addEventListener("click", (e) => {
 | 
			
		||||
      if (e.target.classList.contains("remove-google-font")) {
 | 
			
		||||
        const idx = parseInt(e.target.dataset.idx, 10);
 | 
			
		||||
        googleFonts.splice(idx, 1);
 | 
			
		||||
        renderGoogleFonts(googleFonts);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Form submit
 | 
			
		||||
  document.getElementById("theme-editor-form").addEventListener("submit", async (e) => {
 | 
			
		||||
    e.preventDefault();
 | 
			
		||||
    const data = {};
 | 
			
		||||
    data.colors = {
 | 
			
		||||
      primary: document.getElementById("color-primary-text").value,
 | 
			
		||||
      primary_dark: document.getElementById("color-primary-dark-text").value,
 | 
			
		||||
      secondary: document.getElementById("color-secondary-text").value,
 | 
			
		||||
      accent: document.getElementById("color-accent-text").value,
 | 
			
		||||
      text_dark: document.getElementById("color-text-dark-text").value,
 | 
			
		||||
      background: document.getElementById("color-background-text").value,
 | 
			
		||||
      browser_color: document.getElementById("color-browser-color-text").value
 | 
			
		||||
    };
 | 
			
		||||
    data.fonts = {
 | 
			
		||||
      primary: {
 | 
			
		||||
        name: document.getElementById("font-primary").value,
 | 
			
		||||
        fallback: document.getElementById("font-primary-fallback").value
 | 
			
		||||
      },
 | 
			
		||||
      secondary: {
 | 
			
		||||
        name: document.getElementById("font-secondary").value,
 | 
			
		||||
        fallback: document.getElementById("font-secondary-fallback").value
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    data.favicon = {
 | 
			
		||||
      path: faviconInput.value
 | 
			
		||||
    };
 | 
			
		||||
    data.google_fonts = [];
 | 
			
		||||
    document.querySelectorAll("#google-fonts-fields .input-field").forEach(field => {
 | 
			
		||||
      const family = field.querySelector('input[name^="google_fonts"][name$="[family]"]').value;
 | 
			
		||||
      const weights = field.querySelector('input[name^="google_fonts"][name$="[weights]"]').value
 | 
			
		||||
        .split(",").map(w => w.trim()).filter(w => w);
 | 
			
		||||
      if (family) data.google_fonts.push({ family, weights });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const res = await fetch("/api/theme-info", {
 | 
			
		||||
      method: "POST",
 | 
			
		||||
      headers: { "Content-Type": "application/json" },
 | 
			
		||||
      body: JSON.stringify({ theme_name: themeInfo.theme_name, theme_yaml: data })
 | 
			
		||||
    });
 | 
			
		||||
    if (res.ok) {
 | 
			
		||||
      showToast("Theme saved!", "success");
 | 
			
		||||
    } else {
 | 
			
		||||
      showToast("Error saving theme.", "error");
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -49,27 +49,27 @@
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Title</label>
 | 
			
		||||
            <input type="text" name="info.title" placeholder="Your site title">
 | 
			
		||||
            <input type="text" name="info.title" placeholder="Your site title" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Subtitle</label>
 | 
			
		||||
            <input type="text" name="info.subtitle" placeholder="Your site subtitle">
 | 
			
		||||
            <input type="text" name="info.subtitle" placeholder="Your site subtitle" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Description</label>
 | 
			
		||||
            <input type="text" name="info.description" placeholder="Your site description">
 | 
			
		||||
            <input type="text" name="info.description" placeholder="Your site description" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Canonical URL</label>
 | 
			
		||||
            <input type="text" name="info.canonical" placeholder="https://yoursite.com">
 | 
			
		||||
            <input type="text" name="info.canonical" placeholder="https://yoursite.com" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Keywords (comma separated)</label>
 | 
			
		||||
            <input type="text" name="info.keywords" placeholder="photo, gallery, photography">
 | 
			
		||||
            <input type="text" name="info.keywords" placeholder="photo, gallery, photography" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Author</label>
 | 
			
		||||
            <input type="text" name="info.author" placeholder="Your Name">
 | 
			
		||||
            <input type="text" name="info.author" placeholder="Your Name" required>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
@@ -79,9 +79,9 @@
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Instagram URL</label>
 | 
			
		||||
            <input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile">
 | 
			
		||||
            <input type="text" name="social.instagram_url" placeholder="https://instagram.com/yourprofile" required>
 | 
			
		||||
            <label class="thumbnail-form-label">Thumbnail</label>
 | 
			
		||||
            <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;">
 | 
			
		||||
            <input type="file" id="thumbnail-upload" accept="image/png,image/jpeg,image/webp" style="display:none;" required>
 | 
			
		||||
            <button type="button" id="choose-thumbnail-btn" class="up-btn">📸 Upload a photo</button>
 | 
			
		||||
            <div class="thumbnail-form">
 | 
			
		||||
            <img id="thumbnail-preview" src="" alt="Thumbnail preview" style="max-width:100px;display:none;">
 | 
			
		||||
@@ -106,11 +106,11 @@
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Copyright</label>
 | 
			
		||||
            <input type="text" name="footer.copyright">
 | 
			
		||||
            <input type="text" name="footer.copyright" required>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Legal Label</label>
 | 
			
		||||
            <input type="text" name="footer.legal_label">
 | 
			
		||||
            <input type="text" name="footer.legal_label" re>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
@@ -143,7 +143,7 @@
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Theme</label>
 | 
			
		||||
            <select name="build.theme" id="theme-select"></select>
 | 
			
		||||
            <select name="build.theme" id="theme-select" required></select>
 | 
			
		||||
            <input type="file" id="theme-upload" webkitdirectory directory multiple style="display:none;">
 | 
			
		||||
            <button type="button" id="choose-theme-btn" class="up-btn">📂 Upload custom theme folder</button>
 | 
			
		||||
            <label class="thumbnail-form-label">Images processing</label>
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ h2 {
 | 
			
		||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
			
		||||
  border: 1px solid #2f2e2e80;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  padding: 0px 20px;
 | 
			
		||||
  padding: 0px 20px 20px 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.upload-section label {
 | 
			
		||||
@@ -491,7 +491,7 @@ h2 {
 | 
			
		||||
  padding: 0 40px 40px 40px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form fieldset {
 | 
			
		||||
fieldset {
 | 
			
		||||
  background-color: rgb(67 67 67 / 26%);
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
			
		||||
@@ -501,7 +501,7 @@ h2 {
 | 
			
		||||
  margin: 32px auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form legend {
 | 
			
		||||
legend {
 | 
			
		||||
  font-size: 1.2em;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
  color: #26c4ff;
 | 
			
		||||
@@ -509,13 +509,13 @@ h2 {
 | 
			
		||||
  letter-spacing: 1px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form .fields {
 | 
			
		||||
.fields {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form .input-field {
 | 
			
		||||
.input-field {
 | 
			
		||||
  flex: 1 1 calc(33.333% - 18px);
 | 
			
		||||
  min-width: 220px;
 | 
			
		||||
  max-width: 100%;
 | 
			
		||||
@@ -525,7 +525,7 @@ h2 {
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form label {
 | 
			
		||||
label {
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: #e3e3e3;
 | 
			
		||||
@@ -533,9 +533,9 @@ h2 {
 | 
			
		||||
  letter-spacing: 0.5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form input,
 | 
			
		||||
#site-info-form textarea,
 | 
			
		||||
#site-info-form select {
 | 
			
		||||
#site-info-form input, #theme-editor-form input,
 | 
			
		||||
#site-info-form textarea, #theme-editor-form textarea,
 | 
			
		||||
#site-info-form select, #theme-editor-form select {
 | 
			
		||||
  /* background: rgba(4, 44, 60, 0.55);*/
 | 
			
		||||
  background: #1f2223;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
@@ -551,23 +551,29 @@ h2 {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form input::placeholder,
 | 
			
		||||
#site-info-form textarea::placeholder {
 | 
			
		||||
#theme-editor-form input::placeholder,
 | 
			
		||||
#site-info-form textarea::placeholder,
 | 
			
		||||
#theme-editor-form textarea::placeholder {
 | 
			
		||||
  color: #585858;
 | 
			
		||||
  font-style: italic;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form input:focus,
 | 
			
		||||
#theme-editor-form input:focus,
 | 
			
		||||
#site-info-form textarea:focus,
 | 
			
		||||
#site-info-form select:focus {
 | 
			
		||||
#theme-editor-form textarea:focus,
 | 
			
		||||
#site-info-form select:focus,
 | 
			
		||||
#theme-editor-form select:focus {
 | 
			
		||||
  border-color: #585858;
 | 
			
		||||
  background: #161616;
 | 
			
		||||
}
 | 
			
		||||
#site-info-form textarea {
 | 
			
		||||
#site-info-form textarea,
 | 
			
		||||
#theme-editor-form textarea {
 | 
			
		||||
  min-height: 60px;
 | 
			
		||||
  resize: vertical;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form input[type="file"] {
 | 
			
		||||
#input[type="file"] {
 | 
			
		||||
  background: none;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  border: none;
 | 
			
		||||
@@ -575,13 +581,13 @@ h2 {
 | 
			
		||||
  margin-top: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form img#thumbnail-preview {
 | 
			
		||||
img#thumbnail-preview {
 | 
			
		||||
  margin-top: 8px;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  border: 1px solid #585858;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button[type="submit"] {
 | 
			
		||||
#site-info-form button[type="submit"], #theme-editor-form button[type="submit"] {
 | 
			
		||||
  background: linear-gradient(135deg, #26c4ff, #016074);
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  font-weight: 700;
 | 
			
		||||
@@ -595,11 +601,11 @@ h2 {
 | 
			
		||||
  transition: background 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button[type="submit"]:hover {
 | 
			
		||||
#site-info-form button[type="submit"]:hover,  #theme-editor-form button[type="submit"]:hover {
 | 
			
		||||
  background: linear-gradient(135deg, #72d9ff, #26657e);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button[type="button"] {
 | 
			
		||||
#site-info-form button[type="button"], #theme-editor-form button[type="button"] {
 | 
			
		||||
  background: #00000000;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
  border: none;
 | 
			
		||||
@@ -612,47 +618,47 @@ h2 {
 | 
			
		||||
  border: 1px solid #585858;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button[type="button"]:hover {
 | 
			
		||||
#site-info-form button[type="button"]:hover, #theme-editor-form button[type="button"]:hover {
 | 
			
		||||
  background: #2d2d2d;
 | 
			
		||||
  color: #fff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 900px) {
 | 
			
		||||
  #site-info-form {
 | 
			
		||||
  #site-info-form, #theme-editor-form {
 | 
			
		||||
    padding: 18px 8px;
 | 
			
		||||
  }
 | 
			
		||||
  #site-info-form .fields,
 | 
			
		||||
  #site-info-form fieldset {
 | 
			
		||||
  .fields,
 | 
			
		||||
  fieldset {
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    gap: 0;
 | 
			
		||||
  }
 | 
			
		||||
  #site-info-form .input-field {
 | 
			
		||||
  .input-field {
 | 
			
		||||
    min-width: 100%;
 | 
			
		||||
    margin-bottom: 12px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph {
 | 
			
		||||
#site-info-form button.remove-menu-item, #site-info-form button.remove-ip-paragraph, #theme-editor-form button.remove-menu-item, #theme-editor-form button.remove-ip-paragraph {
 | 
			
		||||
  margin-top: 0px;
 | 
			
		||||
  margin-bottom: 4px;
 | 
			
		||||
  border-radius: 30px;
 | 
			
		||||
  background: #2d2d2d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover {
 | 
			
		||||
#site-info-form button.remove-menu-item:hover, #site-info-form button.remove-ip-paragraph:hover, #theme-editor-form button.remove-menu-item:hover, #theme-editor-form button.remove-ip-paragraph:hover {
 | 
			
		||||
  background: rgb(121, 26, 19);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button.remove-btn {
 | 
			
		||||
#site-info-form button.remove-btn, #theme-editor-form button.remove-btn {
 | 
			
		||||
 | 
			
		||||
  border-radius: 30px;
 | 
			
		||||
  background: #2d2d2d;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form button.remove-btn:hover{
 | 
			
		||||
#site-info-form button.remove-btn:hover, #theme-editor-form button.remove-btn:hover {
 | 
			
		||||
  background: rgb(121, 26, 19);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#site-info-form .thumbnail-form-label {
 | 
			
		||||
#site-info-form .thumbnail-form-label, #theme-editor-form .thumbnail-form-label {
 | 
			
		||||
  margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										178
									
								
								src/webui/theme-editor/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/webui/theme-editor/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1">
 | 
			
		||||
  <meta charset="UTF-8">
 | 
			
		||||
  <title>Theme Editor</title>
 | 
			
		||||
  <link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <!-- Top bar -->
 | 
			
		||||
  <div class="nav-bar">
 | 
			
		||||
    <div class="content-inner nav">
 | 
			
		||||
      <div class="nav-cta">
 | 
			
		||||
        <div class="arrow">→</div>
 | 
			
		||||
        <a class="button" href="/site-info">
 | 
			
		||||
          <span id="step">← Back to Site Info</span>
 | 
			
		||||
        </a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <input type="checkbox" id="nav-check">
 | 
			
		||||
      <div class="nav-header">
 | 
			
		||||
        <div class="nav-title">
 | 
			
		||||
          <img src="../img/logo.svg">
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-btn">
 | 
			
		||||
        <label for="nav-check">
 | 
			
		||||
          <span></span>
 | 
			
		||||
          <span></span>
 | 
			
		||||
          <span></span>
 | 
			
		||||
        </label>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="nav-links">
 | 
			
		||||
        <ul class="nav-list">
 | 
			
		||||
          <li class="nav-item appear2"><a href="/site-info">Site info</a></li>
 | 
			
		||||
          <li class="nav-item appear2"><a href="/theme-editor">Theme editor</a></li>
 | 
			
		||||
          <li class="nav-item appear2"><a href="#">Gallery</a></li>
 | 
			
		||||
        </ul>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Toast container for notifications -->
 | 
			
		||||
  <div id="theme-editor" class="content-inner">
 | 
			
		||||
    <div id="toast-container"></div>
 | 
			
		||||
    <h1>Edit Theme</h1>
 | 
			
		||||
    <!-- Show current theme -->
 | 
			
		||||
    <div class="theme-info">
 | 
			
		||||
      <strong>Current theme:</strong> <span id="current-theme"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
    <form id="theme-editor-form">
 | 
			
		||||
      <!-- Colors Section -->
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <h2>Colors</h2>
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Primary</label>
 | 
			
		||||
            <input type="color" name="colors.primary" id="color-primary">
 | 
			
		||||
            <input type="text" name="colors.primary_text" id="color-primary-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Primary Dark</label>
 | 
			
		||||
            <input type="color" name="colors.primary_dark" id="color-primary-dark">
 | 
			
		||||
            <input type="text" name="colors.primary_dark_text" id="color-primary-dark-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Secondary</label>
 | 
			
		||||
            <input type="color" name="colors.secondary" id="color-secondary">
 | 
			
		||||
            <input type="text" name="colors.secondary_text" id="color-secondary-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Accent</label>
 | 
			
		||||
            <input type="color" name="colors.accent" id="color-accent">
 | 
			
		||||
            <input type="text" name="colors.accent_text" id="color-accent-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Text Dark</label>
 | 
			
		||||
            <input type="color" name="colors.text_dark" id="color-text-dark">
 | 
			
		||||
            <input type="text" name="colors.text_dark_text" id="color-text-dark-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Background</label>
 | 
			
		||||
            <input type="color" name="colors.background" id="color-background">
 | 
			
		||||
            <input type="text" name="colors.background_text" id="color-background-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Browser Color</label>
 | 
			
		||||
            <input type="color" name="colors.browser_color" id="color-browser-color">
 | 
			
		||||
            <input type="text" name="colors.browser_color_text" id="color-browser-color-text" style="width:100px;">
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <!-- Google Fonts Section -->
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <h2>Google Fonts</h2>
 | 
			
		||||
        <div class="fields" id="google-fonts-fields">
 | 
			
		||||
          <!-- JS will render font family and weights inputs here -->
 | 
			
		||||
        </div>
 | 
			
		||||
        <button type="button" id="add-google-font">Add Google Font</button>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <!-- Custom Font Upload Section -->
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <h2>Upload Custom Font (.woff, .woff2)</h2>
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <input type="file" id="font-upload" accept=".woff,.woff2" style="display:none;">
 | 
			
		||||
          <button type="button" id="choose-font-btn" class="up-btn">🖋️ Upload font</button>
 | 
			
		||||
          <div id="local-fonts-list" class="font-list"></div>
 | 
			
		||||
          <span id="font-upload-status"></span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <!-- Fonts Section -->
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <h2>Fonts</h2>
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Primary Font</label>
 | 
			
		||||
            <select name="fonts.primary.name" id="font-primary"></select>
 | 
			
		||||
            <label>Fallback</label>
 | 
			
		||||
            <select name="fonts.primary.fallback" id="font-primary-fallback">
 | 
			
		||||
              <option value="sans-serif">sans-serif</option>
 | 
			
		||||
              <option value="serif">serif</option>
 | 
			
		||||
            </select>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Secondary Font</label>
 | 
			
		||||
            <select name="fonts.secondary.name" id="font-secondary"></select>
 | 
			
		||||
            <label>Fallback</label>
 | 
			
		||||
            <select name="fonts.secondary.fallback" id="font-secondary-fallback">
 | 
			
		||||
              <option value="sans-serif">sans-serif</option>
 | 
			
		||||
              <option value="serif">serif</option>
 | 
			
		||||
            </select>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <!-- Favicon Section -->
 | 
			
		||||
      <fieldset>
 | 
			
		||||
        <h2>Favicon</h2>
 | 
			
		||||
        <div class="fields">
 | 
			
		||||
          <div class="input-field">
 | 
			
		||||
            <label>Favicon Path</label>
 | 
			
		||||
            <input type="text" name="favicon.path" id="favicon-path" readonly>
 | 
			
		||||
            <input type="file" id="favicon-upload" accept=".png,.jpg,.jpeg,.ico" style="display:none;">
 | 
			
		||||
            <button type="button" id="choose-favicon-btn" class="up-btn">🖼️ Upload favicon</button>
 | 
			
		||||
            <div class="favicon-form">
 | 
			
		||||
              <img id="favicon-preview" src="" alt="Favicon preview" style="max-width:48px;display:none;">
 | 
			
		||||
              <button type="button" id="remove-favicon-btn" class="remove-btn up-btn danger" style="display:none;">Remove</button>
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </fieldset>
 | 
			
		||||
      <button type="submit">Save Theme</button>
 | 
			
		||||
    </form>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Delete confirmation modal for favicon -->
 | 
			
		||||
  <div id="delete-favicon-modal" class="modal" style="display:none;">
 | 
			
		||||
    <div class="modal-content">
 | 
			
		||||
      <span id="delete-favicon-modal-close" class="modal-close">×</span>
 | 
			
		||||
      <h3>Confirm Deletion</h3>
 | 
			
		||||
      <p id="delete-favicon-modal-text">Are you sure you want to remove this favicon?</p>
 | 
			
		||||
      <div class="modal-actions">
 | 
			
		||||
        <button id="delete-favicon-modal-confirm" class="modal-btn danger">Remove</button>
 | 
			
		||||
        <button id="delete-favicon-modal-cancel" class="modal-btn">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <!-- Delete confirmation modal for font -->
 | 
			
		||||
  <div id="delete-font-modal" class="modal" style="display:none;">
 | 
			
		||||
    <div class="modal-content">
 | 
			
		||||
      <span id="delete-font-modal-close" class="modal-close">×</span>
 | 
			
		||||
      <h3>Confirm Deletion</h3>
 | 
			
		||||
      <p id="delete-font-modal-text">Are you sure you want to remove this font?</p>
 | 
			
		||||
      <div class="modal-actions">
 | 
			
		||||
        <button id="delete-font-modal-confirm" class="modal-btn danger">Remove</button>
 | 
			
		||||
        <button id="delete-font-modal-cancel" class="modal-btn">Cancel</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <script src="{{ url_for('static', filename='js/theme-editor.js') }}"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
		Reference in New Issue
	
	Block a user