diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css index 0c7b847..f1ee821 100644 --- a/shaarli-pro/css/custom_views.css +++ b/shaarli-pro/css/custom_views.css @@ -1092,41 +1092,6 @@ body.view-notes .content-container { color: inherit; } -.bg-studio-swatches { - display: grid; - grid-template-columns: repeat(6, 26px); - grid-auto-rows: 26px; - gap: 8px; - min-width: 0; - width: 100%; - overflow: hidden; -} - -.bg-studio-swatch { - width: 26px; - height: 26px; - border-radius: 999px; - border: 1px solid rgba(15, 23, 42, 0.22); - cursor: pointer; - padding: 0; - transition: transform 0.12s ease, box-shadow 0.12s ease; - flex: 0 0 auto; -} - -.bg-studio-swatch:hover { - transform: scale(1.15); - box-shadow: 0 10px 20px rgba(0,0,0,0.35); -} - -.bg-studio-swatch.is-active { - border: 2px solid rgba(255, 255, 255, 0.95); - box-shadow: 0 8px 16px rgba(0,0,0,0.35); -} - -[data-theme="dark"] .bg-studio-swatch { - border-color: rgba(255, 255, 255, 0.28); -} - .bg-studio-filters { display: flex; align-items: center; @@ -1161,18 +1126,25 @@ body.view-notes .content-container { } .bg-studio-reset { + width: 28px; height: 28px; - padding: 0 12px; border-radius: 999px; border: 1px solid rgba(15, 23, 42, 0.18); background: linear-gradient(180deg, #ffffff, #f3f4f6); color: #111827; font-size: 0.82rem; font-weight: 600; - letter-spacing: 0.01em; cursor: pointer; box-shadow: 0 1px 2px rgba(15, 23, 42, 0.16); transition: background 0.16s ease, border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.bg-studio-reset i { + font-size: 16px; + color: inherit; } .bg-studio-reset:hover { @@ -1478,6 +1450,15 @@ body.view-notes .content-container { color: var(--note-card-fg, #202124); } +.link-outer { + color: var(--note-card-fg, inherit); +} + +.link-outer .link-title, +.link-outer .link-description { + color: var(--note-card-fg, inherit); +} + /* --- NOTES: Vue Masonry/Grid (multi-colonnes, largeur variable) --- */ .notes-masonry .note-card.note-has-bg { /* Les cartes masonry ont des largeurs variables selon les breakpoints: @@ -1730,4 +1711,355 @@ body.view-notes .content-container { [data-theme="dark"] .palette-btn-filter i, [data-theme="dark"] .palette-btn-filter-none i { color: #e8eaed; +} + +/* ========================================= + Custom Color Picker Panel + ========================================= */ +.color-picker-panel { + position: fixed; + z-index: 1300; + width: min(320px, calc(100vw - 24px)); + border-radius: 20px; + background: rgba(255, 255, 255, 0.95); + border: 1px solid var(--border, #d1d5db); + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.35); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + color: var(--text-main, #0f172a); + padding: 20px; + display: none; +} + +.color-picker-panel.open { + display: block; +} + +[data-theme="dark"] .color-picker-panel { + background: rgba(31, 37, 41, 0.95); + border-color: #2A3238; + box-shadow: 0 28px 80px rgba(0, 0, 0, 0.65); + color: rgba(255, 255, 255, 0.95); +} + +.color-picker-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; +} + +.color-picker-title { + font-size: 16px; + font-weight: 600; + letter-spacing: 0.01em; +} + +.color-picker-close { + width: 32px; + height: 32px; + border-radius: 10px; + border: 1px solid rgba(15, 23, 42, 0.16); + background: rgba(15, 23, 42, 0.05); + color: inherit; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + transition: background 0.15s ease; +} + +.color-picker-close:hover { + background: rgba(15, 23, 42, 0.12); +} + +[data-theme="dark"] .color-picker-close { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.08); +} + +[data-theme="dark"] .color-picker-close:hover { + background: rgba(255, 255, 255, 0.14); +} + +.color-picker-body { + display: flex; + flex-direction: column; + gap: 16px; +} + +.color-picker-gradient { + width: 100%; + height: 140px; + border-radius: 12px; + overflow: hidden; + position: relative; + border: 1px solid rgba(15, 23, 42, 0.12); +} + +[data-theme="dark"] .color-picker-gradient { + border-color: rgba(255, 255, 255, 0.12); +} + +.color-picker-gradient-inner { + width: 100%; + height: 100%; + position: relative; + background: linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(210, 100%, 50%)); + cursor: crosshair; +} + +.color-picker-cursor { + position: absolute; + width: 16px; + height: 16px; + border: 2px solid white; + border-radius: 50%; + box-shadow: 0 2px 6px rgba(0,0,0,0.4), inset 0 0 0 1px rgba(0,0,0,0.2); + transform: translate(-50%, -50%); + pointer-events: none; +} + +.color-picker-hue { + width: 100%; +} + +.color-picker-hue-slider { + width: 100%; + height: 12px; + border-radius: 6px; + -webkit-appearance: none; + appearance: none; + background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%); + outline: none; + cursor: pointer; +} + +.color-picker-hue-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid rgba(15, 23, 42, 0.3); + box-shadow: 0 2px 6px rgba(0,0,0,0.3); + cursor: pointer; +} + +.color-picker-hue-slider::-moz-range-thumb { + width: 18px; + height: 18px; + border-radius: 50%; + background: white; + border: 2px solid rgba(15, 23, 42, 0.3); + box-shadow: 0 2px 6px rgba(0,0,0,0.3); + cursor: pointer; +} + +.color-picker-preview-row { + display: flex; + align-items: center; + gap: 12px; +} + +.color-picker-preview { + width: 48px; + height: 48px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.12); + background: #3b82f6; + flex-shrink: 0; +} + +[data-theme="dark"] .color-picker-preview { + border-color: rgba(255, 255, 255, 0.12); +} + +.color-picker-inputs { + flex: 1; +} + +.color-picker-input-group { + display: flex; + flex-direction: column; + gap: 4px; +} + +.color-picker-input-group label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: rgba(15, 23, 42, 0.6); +} + +[data-theme="dark"] .color-picker-input-group label { + color: rgba(255, 255, 255, 0.6); +} + +.color-picker-input-group input { + width: 100%; + padding: 8px 12px; + border-radius: 8px; + border: 1px solid rgba(15, 23, 42, 0.16); + background: rgba(15, 23, 42, 0.05); + color: inherit; + font-size: 14px; + font-family: monospace; + text-transform: uppercase; + outline: none; + transition: border-color 0.15s ease; +} + +.color-picker-input-group input:focus { + border-color: #3b82f6; +} + +[data-theme="dark"] .color-picker-input-group input { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.08); +} + +.color-picker-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid rgba(15, 23, 42, 0.1); +} + +[data-theme="dark"] .color-picker-footer { + border-color: rgba(255, 255, 255, 0.12); +} + +.color-picker-btn { + padding: 8px 16px; + border-radius: 8px; + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.15s ease; + border: none; +} + +.color-picker-btn-secondary { + background: rgba(15, 23, 42, 0.08); + color: inherit; +} + +.color-picker-btn-secondary:hover { + background: rgba(15, 23, 42, 0.14); +} + +[data-theme="dark"] .color-picker-btn-secondary { + background: rgba(255, 255, 255, 0.1); +} + +[data-theme="dark"] .color-picker-btn-secondary:hover { + background: rgba(255, 255, 255, 0.16); +} + +.color-picker-btn-primary { + background: #3b82f6; + color: white; +} + +.color-picker-btn-primary:hover { + background: #2563eb; +} + +/* ========================================= + Smaller swatches for Colors and Font sections + ========================================= */ +.bg-studio-swatches { + display: grid; + grid-template-columns: repeat(6, 22px); + grid-auto-rows: 22px; + gap: 6px; + min-width: 0; + width: 100%; + overflow: hidden; +} + +.bg-studio-swatch { + width: 22px; + height: 22px; + border-radius: 999px; + border: 1px solid rgba(15, 23, 42, 0.22); + cursor: pointer; + padding: 0; + transition: transform 0.12s ease, box-shadow 0.12s ease; + flex: 0 0 auto; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.bg-studio-swatch i { + font-size: 14px; + color: rgba(15, 23, 42, 0.6); + pointer-events: none; +} + +[data-theme="dark"] .bg-studio-swatch i { + color: rgba(255, 255, 255, 0.7); +} + +.bg-studio-swatch:hover { + transform: scale(1.12); + box-shadow: 0 6px 16px rgba(0,0,0,0.3); +} + +.bg-studio-swatch.is-active { + border: 2px solid rgba(255, 255, 255, 0.95); + box-shadow: 0 4px 12px rgba(0,0,0,0.3); +} + +.bg-studio-swatch-custom { + background: linear-gradient(135deg, rgba(15, 23, 42, 0.08), rgba(15, 23, 42, 0.04)); +} + +[data-theme="dark"] .bg-studio-swatch-custom { + background: linear-gradient(135deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.06)); +} + +.bg-studio-swatch-custom:hover { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(59, 130, 246, 0.08)); +} + +[data-theme="dark"] .bg-studio-swatch { + border-color: rgba(255, 255, 255, 0.28); +} + +/* Font swatches - slightly smaller */ +.bg-studio-swatches-font { + grid-template-columns: repeat(6, 20px); + grid-auto-rows: 20px; + gap: 5px; +} + +.bg-studio-swatches-font .bg-studio-swatch { + width: 20px; + height: 20px; +} + +.bg-studio-swatches-font .bg-studio-swatch i { + font-size: 12px; +} + +/* Custom note color style */ +.note-card.note-color-custom, +.note-modal.note-color-custom { + border-color: transparent; +} + +/* Ensure Font section is properly sized */ +.bg-studio-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 10px; } \ No newline at end of file diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js index 1676a31..04e9f4c 100644 --- a/shaarli-pro/js/custom_views.js +++ b/shaarli-pro/js/custom_views.js @@ -116,6 +116,12 @@ const NOTE_COLOR_OPTIONS = [ label: "Gris", light: "#e8eaed", dark: "#4e5764" + }, + { + key: "custom", + label: "Personnalisé", + light: "#custom", + dark: "#custom" } ]; @@ -130,6 +136,18 @@ const NOTE_FILTER_OPTIONS = [ { key: "stripes", label: "Rayures", icon: "mdi-slash-forward" }, ]; +const NOTE_FONT_COLOR_OPTIONS = [ + { key: "auto", label: "Auto", value: "auto" }, + { key: "light", label: "Clair", value: "#f5f7fb" }, + { key: "dark", label: "Sombre", value: "#202124" }, + { key: "white", label: "Blanc", value: "#ffffff" }, + { key: "black", label: "Noir", value: "#000000" }, + { key: "custom", label: "Personnalisé", value: "custom" } +]; + +const NOTE_FONT_COLOR_TAG_PREFIX = "font-"; +const NOTE_COLOR_TAG_PREFIX = "note-color-"; + const NOTE_FILTER_TAG_PREFIX = "notefilter-"; const NOTE_BACKGROUND_TAG_PREFIX = "notebg-"; @@ -358,11 +376,13 @@ function getNoteBackgroundUrl(backgroundKey, mode = getCurrentThemeMode()) { function getElementVisualColor(element) { if (!element) return "default"; - const colorClass = Array.from(element.classList).find((cls) => cls.startsWith("note-color-")); - if (colorClass) { - return colorClass.replace("note-color-", "") || "default"; + if (element.dataset.color === "custom" && element.dataset.customColor) { + return `custom:${element.dataset.customColor}`; } + const colorClass = Array.from(element.classList).find((cls) => cls.startsWith("note-color-")); + if (colorClass) return colorClass.replace("note-color-", ""); + return element.dataset.color || "default"; } @@ -387,11 +407,18 @@ function getElementVisualFilter(element) { return normalizeFilterKey(element.dataset.filter || ""); } +function getElementVisualFontColor(element) { + if (!element) return "auto"; + return element.dataset.fontColor || "auto"; +} + function refreshNoteFilterVisuals() { document.querySelectorAll(".note-card, .note-modal, .link-outer").forEach((element) => { applyNoteVisualState(element, { color: getElementVisualColor(element), filter: getElementVisualFilter(element), + background: getElementVisualBackground(element), + fontColor: getElementVisualFontColor(element), }); }); } @@ -616,6 +643,32 @@ function ensureBackgroundStudioPanel() { } return; } + + if (action === "reset-font-color") { + if (mode === "modal") { + setModalNoteFontColor("auto"); + } else { + setNoteFontColor(entityId, "auto", editUrl); + } + return; + } + + if (action === "open-color-picker") { + openColorPickerPanel({ mode, entityId, editUrl, type: "background" }); + return; + } + + if (action === "open-font-color-picker") { + openColorPickerPanel({ mode, entityId, editUrl, type: "font" }); + return; + } + + if (action === "set-font-color") { + const fontColorKey = actionBtn.dataset.fontColorKey || "auto"; + if (mode === "modal") setModalNoteFontColor(fontColorKey); + else setNoteFontColor(entityId, fontColorKey, editUrl); + return; + } }); panel.addEventListener("keydown", (e) => { @@ -709,6 +762,7 @@ function openBackgroundStudioPanel({ currentColor, currentFilter, currentBackground, + currentFontColor, title, }) { ensureBackgroundStudioPanel(); @@ -721,6 +775,7 @@ function openBackgroundStudioPanel({ panel.dataset.color = currentColor || "default"; panel.dataset.filter = normalizeFilterKey(currentFilter || "") || "none"; panel.dataset.background = normalizeBackgroundKey(currentBackground || "") || "none"; + panel.dataset.fontColor = currentFontColor || panel.dataset.fontColor || "auto"; panel.dataset.query = panel.dataset.query || ""; panel.dataset.title = title || "Mes images & couleurs"; panel.__anchorEl = anchorEl || null; @@ -841,7 +896,9 @@ function renderBackgroundStudioPanel(panel) { const title = panel.dataset.title || "Mes images & couleurs"; const color = panel.dataset.color || "default"; + const isCustomColor = typeof color === "string" && color.startsWith("custom:"); const currentFilter = panel.dataset.filter || "none"; + const currentFontColor = panel.dataset.fontColor || "auto"; const query = normalizeSearchText(panel.dataset.query || ""); const { colors, backgrounds } = getBackgroundStudioItems(); @@ -867,14 +924,38 @@ function renderBackgroundStudioPanel(panel) { }) .join(""); + // Colors row with custom color picker button const colorsRowHtml = colors - .filter((c) => c.key !== "default") + .filter((c) => c.key !== "default" && c.key !== "custom") .map((c) => { const isActive = color === c.key; const hex = c.color || ""; return ``; }) - .join(""); + .join("") + + // Add custom color button (opens color picker) + ``; + + // Font colors row + const isCustomFontColor = typeof currentFontColor === "string" && currentFontColor.startsWith("custom:"); + const fontColorsRowHtml = NOTE_FONT_COLOR_OPTIONS + .filter((c) => c.key !== "custom") + .map((c) => { + const isActive = currentFontColor === c.key; + const value = c.value || "auto"; + const style = + value === "auto" + ? "background: linear-gradient(135deg, #f5f7fb 50%, #202124 50%);" + : `background-color: ${value};`; + return ``; + }) + .join("") + + // Add custom font color button + ``; const filterBtn = (key, icon, label) => { const active = currentFilter === key; @@ -895,10 +976,17 @@ function renderBackgroundStudioPanel(panel) {
Colors:
- +
${colorsRowHtml}
+
+
+
Font:
+ +
+
${fontColorsRowHtml}
+
Filtres:
${filtersHtml}
@@ -932,13 +1020,18 @@ function renderBackgroundStudioPanel(panel) { function applyNoteVisualState(element, note) { if (!element || !note) return; - const resolvedColorOption = getColorOption(note.color || "default"); - const color = resolvedColorOption ? resolvedColorOption.key : "default"; - const colorValue = getThemeColorValue(resolvedColorOption); + const rawColor = note.color || "default"; + const isCustomColor = typeof rawColor === "string" && rawColor.startsWith("custom:"); + const customHex = isCustomColor ? rawColor.substring(7) : ""; + + const resolvedColorOption = isCustomColor ? null : getColorOption(rawColor); + const color = isCustomColor ? "custom" : (resolvedColorOption ? resolvedColorOption.key : "default"); + const colorValue = isCustomColor ? customHex : getThemeColorValue(resolvedColorOption); const foregroundColor = getReadableForegroundForBackground(colorValue); const filter = normalizeFilterKey(note.filter || "none"); const normalizedBackground = normalizeBackgroundKey(note.background || ""); const background = normalizedBackground || "none"; + const fontColor = note.fontColor || "auto"; element.classList.forEach((cls) => { if (cls.startsWith("note-color-")) element.classList.remove(cls); @@ -960,6 +1053,19 @@ function applyNoteVisualState(element, note) { element.style.setProperty("--note-card-fg", foregroundColor); } + if (fontColor && fontColor !== "auto") { + let fontValue = null; + if (typeof fontColor === "string" && fontColor.startsWith("custom:")) { + fontValue = fontColor.substring(7); + } else { + const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === fontColor); + if (opt && opt.value && opt.value !== "auto") fontValue = opt.value; + } + if (fontValue) { + element.style.setProperty("--note-card-fg", fontValue); + } + } + if (background && background !== "none") { const bgUrl = getNoteBackgroundUrl(background); if (bgUrl) { @@ -978,19 +1084,45 @@ function applyNoteVisualState(element, note) { } element.dataset.color = color; + if (isCustomColor && customHex) { + element.dataset.customColor = customHex; + } else { + element.dataset.customColor = ""; + } element.dataset.filter = filter; + element.dataset.fontColor = fontColor; } function extractNoteVisualStateFromTags(tags) { const safeTags = Array.isArray(tags) ? tags : []; let color = "default"; - const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-")); + const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_COLOR_TAG_PREFIX)); if (foundColorTag) { - const candidate = foundColorTag.substring(5); - if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { + const candidate = foundColorTag.substring(NOTE_COLOR_TAG_PREFIX.length); + if (/^[0-9A-Fa-f]{6}$/.test(candidate)) { + color = `custom:#${candidate.toUpperCase()}`; + } else if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { color = candidate; } + } else { + // Backward compat: legacy note-custom- + const legacyCustomTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-custom-")); + if (legacyCustomTag) { + const candidate = legacyCustomTag.substring("note-custom-".length); + if (/^[0-9A-Fa-f]{6}$/.test(candidate)) { + color = `custom:#${candidate.toUpperCase()}`; + } + } + + // Backward compat: legacy note- + const legacyColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-")); + if (legacyColorTag) { + const candidate = legacyColorTag.substring(5); + if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) { + color = candidate; + } + } } let filter = "none"; @@ -1011,7 +1143,17 @@ function extractNoteVisualStateFromTags(tags) { } } - return { color, filter, background }; + let fontColor = "auto"; + const foundFontTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)); + if (foundFontTag) { + const raw = foundFontTag.substring(NOTE_FONT_COLOR_TAG_PREFIX.length); + const hex = raw.startsWith("#") ? raw : `#${raw}`; + if (/^#[0-9A-Fa-f]{6}$/.test(hex)) { + fontColor = `custom:${hex.toUpperCase()}`; + } + } + + return { color, filter, background, fontColor }; } function initBookmarkPaletteButtons() { @@ -1023,8 +1165,8 @@ function initBookmarkPaletteButtons() { if (!cardId) return; const tags = Array.from(card.querySelectorAll(".link-tag a")).map((a) => (a.textContent || "").trim()); - const { color, filter, background } = extractNoteVisualStateFromTags(tags); - applyNoteVisualState(card, { color, filter, background }); + const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(tags); + applyNoteVisualState(card, { color, filter, background, fontColor }); const actions = card.querySelector(".link-actions"); if (!actions) return; @@ -1066,6 +1208,7 @@ function initBookmarkPaletteButtons() { currentColor: getElementVisualColor(card), currentFilter: getElementVisualFilter(card), currentBackground: getElementVisualBackground(card), + currentFontColor: getElementVisualFontColor(card), title: "Mes images & couleurs", }); }); @@ -1497,6 +1640,7 @@ function initNoteView(linkList, container) { currentColor: modalCard ? getElementVisualColor(modalCard) : "default", currentFilter: modalCard ? getElementVisualFilter(modalCard) : "none", currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none", + currentFontColor: modalCard ? getElementVisualFontColor(modalCard) : "auto", title: "Mes images & couleurs", }); }); @@ -1611,28 +1755,29 @@ function parseNoteFromLink(linkEl) { const urlEl = linkEl.querySelector(".link-url"); const url = urlEl ? urlEl.textContent.trim() : ""; - const tags = []; - let color = "default"; - let filter = "none"; - let background = "none"; + const rawTags = []; linkEl.querySelectorAll(".link-tag-list a").forEach((tag) => { - const t = tag.textContent.trim(); - // Check for color tag - if (t.startsWith("note-")) { - const potentialColor = t.substring(5); - const knownColor = NOTE_COLOR_OPTIONS.some((opt) => opt.key === potentialColor); - if (knownColor) { - color = potentialColor; - } else { - tags.push(t); - } - } else if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) { - filter = normalizeFilterKey(t.substring(NOTE_FILTER_TAG_PREFIX.length)) || "none"; - } else if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) { - background = normalizeBackgroundKey(t.substring(NOTE_BACKGROUND_TAG_PREFIX.length)) || "none"; - } else { - tags.push(t); - } + const t = (tag.textContent || "").trim(); + if (t) rawTags.push(t); + }); + + const { color, filter, background, fontColor } = extractNoteVisualStateFromTags(rawTags); + + const isLegacyNoteColorTag = (t) => { + if (typeof t !== "string") return false; + if (!t.startsWith("note-")) return false; + const candidate = t.substring(5); + return NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate); + }; + + const tags = rawTags.filter((t) => { + if (t.startsWith(NOTE_COLOR_TAG_PREFIX)) return false; + if (t.startsWith("note-custom-")) return false; + if (isLegacyNoteColorTag(t)) return false; + if (t.startsWith(NOTE_FILTER_TAG_PREFIX)) return false; + if (t.startsWith(NOTE_BACKGROUND_TAG_PREFIX)) return false; + if (t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)) return false; + return true; }); const isPinnedByTag = tags.includes("shaarli-pin"); @@ -1645,7 +1790,7 @@ function parseNoteFromLink(linkEl) { // User requested "availability of the tag 'shaarli-pin' as the main source" const isPinned = tags.includes("shaarli-pin"); - return { id, title, descHtml, descText, coverImage, url, tags, color, filter, background, editUrl, deleteUrl, pinUrl, isPinned }; + return { id, title, descHtml, descText, coverImage, url, tags, color, filter, background, fontColor, editUrl, deleteUrl, pinUrl, isPinned }; } function renderNotes(container, notes, viewMode) { @@ -1907,19 +2052,15 @@ function generatePaletteButtons(note) { window.setNoteColor = function (noteId, color, editUrl) { // 1. Visual Update (Immediate feedback) - const card = document.querySelector(`.note-card[data-id="${noteId}"]`); - if (card) { - const filter = card.dataset.filter || "none"; - applyNoteVisualState(card, { color, filter }); - } + setNoteColorVisual(noteId, color); const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); if (bookmarkCard) { - const filter = bookmarkCard.dataset.filter || "none"; - applyNoteVisualState(bookmarkCard, { color, filter }); const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup"); if (palettePopup) { - palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, filter); + const currentColor = getElementVisualColor(bookmarkCard); + const currentFilter = getElementVisualFilter(bookmarkCard); + palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, currentColor, currentFilter); } } @@ -1962,7 +2103,13 @@ window.setNoteColor = function (noteId, color, editUrl) { let currentTags = formData.get("lf_tags") || ""; let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== ""); - // Remove existing color tags + // Remove existing color tags (only one note-color-* allowed) + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX)); + + // Backward compat cleanup: remove legacy note-custom- + tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-")); + + // Backward compat cleanup: remove legacy note- tagsArray = tagsArray.filter((t) => { if (!t.startsWith("note-")) return true; const colorKey = t.substring(5); @@ -1971,7 +2118,12 @@ window.setNoteColor = function (noteId, color, editUrl) { // Add new color tag (unless default) if (color !== "default") { - tagsArray.push(`note-${color}`); + if (typeof color === "string" && color.startsWith("custom:")) { + const cleanColor = color.substring(7).replace("#", ""); + tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`); + } else { + tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${color}`); + } } formData.set("lf_tags", tagsArray.join(" ")); @@ -2503,3 +2655,661 @@ function togglePinTag(id, editUrl, btn) { }) .catch((err) => console.error(err)); } + +/* ========================================================== + COLOR PICKER PANEL + ========================================================== */ +let colorPickerPanelInitialized = false; + +function ensureColorPickerPanel() { + if (colorPickerPanelInitialized) return; + if (document.getElementById("shaarli-color-picker")) { + colorPickerPanelInitialized = true; + return; + } + + const panel = document.createElement("div"); + panel.id = "shaarli-color-picker"; + panel.className = "color-picker-panel"; + panel.setAttribute("role", "dialog"); + panel.setAttribute("aria-modal", "false"); + panel.setAttribute("aria-hidden", "true"); + panel.style.display = "none"; + document.body.appendChild(panel); + + document.addEventListener("click", (e) => { + const p = document.getElementById("shaarli-color-picker"); + if (!p || !p.classList.contains("open")) return; + if (e.target.closest("#shaarli-color-picker")) return; + if (e.target.closest("#shaarli-bg-studio")) return; + closeColorPickerPanel(); + }); + + document.addEventListener("keydown", (e) => { + if (e.key !== "Escape") return; + const p = document.getElementById("shaarli-color-picker"); + if (!p || !p.classList.contains("open")) return; + closeColorPickerPanel(); + }); + + colorPickerPanelInitialized = true; +} + +function closeColorPickerPanel() { + const panel = document.getElementById("shaarli-color-picker"); + if (!panel) return; + + const type = panel.dataset.type || "background"; + const skipRestore = panel.dataset.skipRestore === "1"; + if (!skipRestore && type === "font") { + const prevFontColorKey = panel.dataset.prevFontColorKey || "auto"; + const mode = panel.dataset.mode || "entity"; + const entityId = panel.dataset.entityId || ""; + if (mode === "modal") { + setModalNoteFontColorVisual(prevFontColorKey); + } else { + setNoteFontColorVisual(entityId, prevFontColorKey); + } + } + + if (!skipRestore && type === "background") { + const prevColorKey = panel.dataset.prevColorKey || "default"; + const mode = panel.dataset.mode || "entity"; + const entityId = panel.dataset.entityId || ""; + if (mode === "modal") { + setModalNoteColorVisual(prevColorKey); + } else { + setNoteColorVisual(entityId, prevColorKey); + } + } + + panel.dataset.skipRestore = "0"; + panel.classList.remove("open"); + panel.style.display = "none"; + panel.setAttribute("aria-hidden", "true"); +} + +function openColorPickerPanel({ mode, entityId, editUrl, type }) { + ensureColorPickerPanel(); + const panel = document.getElementById("shaarli-color-picker"); + if (!panel) return; + + panel.dataset.mode = mode || "entity"; + panel.dataset.entityId = entityId || ""; + panel.dataset.editUrl = editUrl || ""; + panel.dataset.type = type || "background"; + + const isDark = getCurrentThemeMode() === "dark"; + const defaultColor = isDark ? "#20293A" : "#ffffff"; + + if ((type || "background") === "font") { + let prevFontColorKey = "auto"; + if ((mode || "entity") === "modal") { + const modal = document.querySelector(".note-modal-overlay"); + const modalCard = modal ? modal.querySelector(".note-modal") : null; + prevFontColorKey = (modalCard && modalCard.dataset.fontColor) ? modalCard.dataset.fontColor : "auto"; + } else { + const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`); + const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`); + prevFontColorKey = (noteCard && noteCard.dataset.fontColor) ? noteCard.dataset.fontColor : ((bookmarkCard && bookmarkCard.dataset.fontColor) ? bookmarkCard.dataset.fontColor : "auto"); + } + panel.dataset.prevFontColorKey = prevFontColorKey; + } else { + panel.dataset.prevFontColorKey = ""; + } + + if ((type || "background") === "background") { + let prevColorKey = "default"; + if ((mode || "entity") === "modal") { + const modal = document.querySelector(".note-modal-overlay"); + const modalCard = modal ? modal.querySelector(".note-modal") : null; + if (modalCard) { + const isCustom = modalCard.dataset.color === "custom"; + const cc = modalCard.dataset.customColor || ""; + prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(modalCard); + } + } else { + const noteCard = document.querySelector(`.note-card[data-id="${entityId}"]`); + const bookmarkCard = document.querySelector(`.link-outer[data-id="${entityId}"]`); + const el = noteCard || bookmarkCard; + if (el) { + const isCustom = el.dataset.color === "custom"; + const cc = el.dataset.customColor || ""; + prevColorKey = isCustom && cc ? `custom:${cc}` : getElementVisualColor(el); + } + } + panel.dataset.prevColorKey = prevColorKey; + } else { + panel.dataset.prevColorKey = ""; + } + + panel.innerHTML = ` +
+
${type === "font" ? "Couleur du texte" : "Couleur personnalisée"}
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+ + +
+
+
+
+ + `; + + // Initialize color picker state + let currentHue = 210; + let currentSaturation = 50; + let currentValue = 50; + let currentHex = defaultColor; + + const gradient = panel.querySelector("#color-gradient"); + const cursor = panel.querySelector("#color-cursor"); + const hueSlider = panel.querySelector("#hue-slider"); + const hexInput = panel.querySelector("#hex-input"); + const preview = panel.querySelector("#color-preview"); + + function hsvToHex(h, s, v) { + const c = v * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = v - c; + let r, g, b; + + if (h < 60) { r = c; g = x; b = 0; } + else if (h < 120) { r = x; g = c; b = 0; } + else if (h < 180) { r = 0; g = c; b = x; } + else if (h < 240) { r = 0; g = x; b = c; } + else if (h < 300) { r = x; g = 0; b = c; } + else { r = c; g = 0; b = x; } + + const toHex = (n) => Math.round((n + m) * 255).toString(16).padStart(2, "0"); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(); + } + + function hexToHsv(hex) { + const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + if (!result) return { h: 0, s: 0, v: 0 }; + + const r = parseInt(result[1], 16) / 255; + const g = parseInt(result[2], 16) / 255; + const b = parseInt(result[3], 16) / 255; + + const max = Math.max(r, g, b); + const min = Math.min(r, g, b); + const d = max - min; + + let h = 0; + if (d !== 0) { + if (max === r) h = ((g - b) / d + 6) % 6 * 60; + else if (max === g) h = ((b - r) / d + 2) * 60; + else h = ((r - g) / d + 4) * 60; + } + + const s = max === 0 ? 0 : d / max; + const v = max; + + return { h, s, v }; + } + + function updateColor() { + currentHex = hsvToHex(currentHue, currentSaturation / 100, currentValue / 100); + gradient.style.background = `linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, hsl(${currentHue}, 100%, 50%))`; + cursor.style.left = `${currentSaturation}%`; + cursor.style.top = `${100 - currentValue}%`; + preview.style.backgroundColor = currentHex; + hexInput.value = currentHex; + + if ((panel.dataset.type || "background") === "font") { + const previewKey = `custom:${currentHex}`; + const pMode = panel.dataset.mode || "entity"; + const pEntityId = panel.dataset.entityId || ""; + if (pMode === "modal") { + setModalNoteFontColorVisual(previewKey); + } else { + setNoteFontColorVisual(pEntityId, previewKey); + } + } + + if ((panel.dataset.type || "background") === "background") { + const previewKey = `custom:${currentHex}`; + const pMode = panel.dataset.mode || "entity"; + const pEntityId = panel.dataset.entityId || ""; + if (pMode === "modal") { + setModalNoteColorVisual(previewKey); + } else { + setNoteColorVisual(pEntityId, previewKey); + } + } + } + + function setFromHex(hex) { + const hsv = hexToHsv(hex); + currentHue = hsv.h; + currentSaturation = hsv.s * 100; + currentValue = hsv.v * 100; + hueSlider.value = currentHue; + updateColor(); + } + + // Event listeners + let isDragging = false; + + gradient.addEventListener("mousedown", (e) => { + isDragging = true; + const rect = gradient.getBoundingClientRect(); + currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); + currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100)); + updateColor(); + }); + + document.addEventListener("mousemove", (e) => { + if (!isDragging) return; + const rect = gradient.getBoundingClientRect(); + currentSaturation = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100)); + currentValue = Math.max(0, Math.min(100, 100 - ((e.clientY - rect.top) / rect.height) * 100)); + updateColor(); + }); + + document.addEventListener("mouseup", () => { + isDragging = false; + }); + + hueSlider.addEventListener("input", (e) => { + currentHue = parseInt(e.target.value); + updateColor(); + }); + + hexInput.addEventListener("input", (e) => { + const hex = e.target.value; + if (/^#[0-9A-F]{6}$/i.test(hex)) { + setFromHex(hex); + } + }); + + // Initialize + if ((panel.dataset.type || "background") === "font") { + const prevFontColorKey = panel.dataset.prevFontColorKey || "auto"; + let initialHex = defaultColor; + if (typeof prevFontColorKey === "string" && prevFontColorKey.startsWith("custom:")) { + initialHex = prevFontColorKey.substring(7); + } else if (prevFontColorKey !== "auto") { + const opt = NOTE_FONT_COLOR_OPTIONS.find((o) => o.key === prevFontColorKey); + if (opt && opt.value && opt.value !== "auto") initialHex = opt.value; + } + setFromHex(initialHex); + } else if ((panel.dataset.type || "background") === "background") { + const prevColorKey = panel.dataset.prevColorKey || "default"; + let initialHex = defaultColor; + if (typeof prevColorKey === "string" && prevColorKey.startsWith("custom:")) { + initialHex = prevColorKey.substring(7); + } + setFromHex(initialHex); + } else { + setFromHex(defaultColor); + } + + // Position the color picker panel + positionColorPickerPanel(panel, mode); + + panel.style.display = "block"; + panel.classList.add("open"); + panel.setAttribute("aria-hidden", "false"); +} + +function positionColorPickerPanel(panel, mode) { + if (!panel) return; + + const bgPanel = document.getElementById("shaarli-bg-studio"); + const viewportPadding = 12; + + // Default position if bgPanel is not available + let anchorRect = null; + + if (bgPanel && bgPanel.classList.contains("open")) { + const bgRect = bgPanel.getBoundingClientRect(); + // Position to the right of the bg-studio panel + let left = bgRect.right + 10; + let top = bgRect.top; + + // Check if it fits on the right + if (left + panel.offsetWidth > window.innerWidth - viewportPadding) { + // Position to the left instead + left = bgRect.left - panel.offsetWidth - 10; + } + + // Ensure it stays within viewport + if (left < viewportPadding) left = viewportPadding; + if (top + panel.offsetHeight > window.innerHeight - viewportPadding) { + top = window.innerHeight - viewportPadding - panel.offsetHeight; + } + if (top < viewportPadding) top = viewportPadding; + + panel.style.left = `${Math.round(left)}px`; + panel.style.top = `${Math.round(top)}px`; + panel.style.right = ""; + panel.style.bottom = ""; + } else { + // Center in viewport if bg panel not available + const panelRect = panel.getBoundingClientRect(); + const left = Math.max(viewportPadding, (window.innerWidth - panelRect.width) / 2); + const top = Math.max(viewportPadding, (window.innerHeight - panelRect.height) / 2); + panel.style.left = `${Math.round(left)}px`; + panel.style.top = `${Math.round(top)}px`; + } +} + +// Handle color picker actions +document.addEventListener("click", (e) => { + const actionBtn = e.target.closest("[data-color-picker-action]"); + if (!actionBtn) return; + + const panel = document.getElementById("shaarli-color-picker"); + if (!panel) return; + + const action = actionBtn.dataset.colorPickerAction; + + if (action === "close") { + closeColorPickerPanel(); + return; + } + + if (action === "apply") { + const mode = panel.dataset.mode || "entity"; + const entityId = panel.dataset.entityId || ""; + const editUrl = panel.dataset.editUrl || ""; + const type = panel.dataset.type || "background"; + const hexInput = panel.querySelector("#hex-input"); + const color = hexInput ? hexInput.value : "#ffffff"; + + if (type === "font") { + if (mode === "modal") { + setModalCustomFontColor(color); + } else { + setNoteFontColor(entityId, `custom:${color}`, editUrl); + } + } else { + if (mode === "modal") { + setModalCustomNoteColor(color); + } else { + setNoteColor(entityId, `custom:${color}`, editUrl); + } + } + + panel.dataset.skipRestore = "1"; + closeColorPickerPanel(); + } +}); + +function setNoteFontColorVisual(noteId, fontColorKey) { + const card = document.querySelector(`.note-card[data-id="${noteId}"]`); + const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); + + let colorValue = "auto"; + if (typeof fontColorKey === "string" && fontColorKey.startsWith("custom:")) { + colorValue = fontColorKey.substring(7); + } else if (fontColorKey !== "auto") { + const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); + colorValue = option ? option.value : "auto"; + } + + const applyTo = (element) => { + if (!element) return; + if (colorValue === "auto") element.style.removeProperty("--note-card-fg"); + else element.style.setProperty("--note-card-fg", colorValue); + element.dataset.fontColor = fontColorKey; + }; + + applyTo(card); + applyTo(bookmarkCard); + + const modal = document.querySelector(".note-modal-overlay"); + if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { + const modalCard = modal.querySelector(".note-modal"); + applyTo(modalCard); + } +} + +function setModalNoteFontColorVisual(fontColorKey) { + const modal = document.querySelector(".note-modal-overlay"); + if (!modal || !modal.currentNote) return; + setNoteFontColorVisual(modal.currentNote.id, fontColorKey); +} + +function setNoteColorVisual(noteId, colorKey) { + const card = document.querySelector(`.note-card[data-id="${noteId}"]`); + const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`); + + const applyTo = (element) => { + if (!element) return; + const currentState = { + color: getElementVisualColor(element), + filter: getElementVisualFilter(element), + background: getElementVisualBackground(element), + fontColor: getElementVisualFontColor(element), + }; + + if (typeof colorKey === "string" && colorKey.startsWith("custom:")) { + const hex = colorKey.substring(7); + element.classList.forEach((cls) => { + if (cls.startsWith("note-color-")) element.classList.remove(cls); + }); + element.classList.add("note-color-custom"); + element.style.backgroundColor = hex; + element.style.borderColor = "transparent"; + element.dataset.color = "custom"; + element.dataset.customColor = hex; + const fg = getReadableForegroundForBackground(hex); + if (fg) element.style.setProperty("--note-card-fg", fg); + return; + } + + element.dataset.customColor = ""; + applyNoteVisualState(element, { ...currentState, color: colorKey }); + }; + + applyTo(card); + applyTo(bookmarkCard); + + const modal = document.querySelector(".note-modal-overlay"); + if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) { + const modalCard = modal.querySelector(".note-modal"); + applyTo(modalCard); + } +} + +function setModalNoteColorVisual(colorKey) { + const modal = document.querySelector(".note-modal-overlay"); + if (!modal || !modal.currentNote) return; + setNoteColorVisual(modal.currentNote.id, colorKey); +} + +/* ========================================================== + FONT COLOR FUNCTIONS + ========================================================== */ +function setNoteFontColor(noteId, fontColorKey, editUrl) { + // 1. Visual Update (Immediate feedback) + setNoteFontColorVisual(noteId, fontColorKey); + + // 2. Persistence via AJAX Form Submission + fetch(editUrl) + .then((response) => response.text()) + .then((html) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const form = doc.querySelector('form[name="linkform"]'); + + if (!form) throw new Error("Could not find edit form"); + + const formData = new URLSearchParams(); + const inputs = form.querySelectorAll("input, textarea"); + + inputs.forEach((input) => { + if (input.type === "checkbox") { + if (input.checked) formData.append(input.name, input.value || "on"); + } else if (input.name) { + formData.append(input.name, input.value); + } + }); + + let currentTags = formData.get("lf_tags") || ""; + let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== ""); + + // Remove existing font color tags + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_FONT_COLOR_TAG_PREFIX)); + + // Add new font color tag (unless auto) + if (fontColorKey !== "auto") { + if (fontColorKey.startsWith("custom:")) { + tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${fontColorKey.substring(7).replace("#", "")}`); + } else { + const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); + if (option && option.value !== "auto") { + tagsArray.push(`${NOTE_FONT_COLOR_TAG_PREFIX}${option.value.substring(1)}`); + } + } + } + + formData.set("lf_tags", tagsArray.join(" ")); + formData.append("save_edit", "1"); + + return fetch(form.action, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formData.toString(), + }); + }) + .then((response) => { + if (response.ok) { + console.log(`Font color ${fontColorKey} saved for note ${noteId}`); + } else { + throw new Error("Failed to save font color"); + } + }) + .catch((err) => { + console.error("Error saving note font color:", err); + }); +} + +function setModalNoteFontColor(fontColorKey) { + const modal = document.querySelector(".note-modal-overlay"); + if (!modal || !modal.currentNote) return; + + const currentNote = modal.currentNote; + setNoteFontColor(currentNote.id, fontColorKey, currentNote.editUrl); + + currentNote.fontColor = fontColorKey; + const modalCard = modal.querySelector(".note-modal"); + if (modalCard) { + let colorValue = "auto"; + if (fontColorKey.startsWith("custom:")) { + colorValue = fontColorKey.substring(7); + } else if (fontColorKey !== "auto") { + const option = NOTE_FONT_COLOR_OPTIONS.find((opt) => opt.key === fontColorKey); + colorValue = option ? option.value : "auto"; + } + + if (colorValue === "auto") { + modalCard.style.removeProperty("--note-card-fg"); + } else { + modalCard.style.setProperty("--note-card-fg", colorValue); + } + modalCard.dataset.fontColor = fontColorKey; + } +} + +function setModalCustomFontColor(color) { + setModalNoteFontColor(`custom:${color}`); +} + +/* ========================================================== + CUSTOM NOTE COLOR FUNCTIONS + ========================================================== */ +function setModalCustomNoteColor(color) { + const modal = document.querySelector(".note-modal-overlay"); + if (!modal || !modal.currentNote) return; + + const currentNote = modal.currentNote; + + // Apply custom color visually + const modalCard = modal.querySelector(".note-modal"); + if (modalCard) { + modalCard.style.backgroundColor = color; + modalCard.style.borderColor = "transparent"; + modalCard.dataset.color = "custom"; + modalCard.dataset.customColor = color; + } + + // Save via AJAX + fetch(currentNote.editUrl) + .then((response) => response.text()) + .then((html) => { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, "text/html"); + const form = doc.querySelector('form[name="linkform"]'); + + if (!form) throw new Error("Could not find edit form"); + + const formData = new URLSearchParams(); + const inputs = form.querySelectorAll("input, textarea"); + + inputs.forEach((input) => { + if (input.type === "checkbox") { + if (input.checked) formData.append(input.name, input.value || "on"); + } else if (input.name) { + formData.append(input.name, input.value); + } + }); + + let currentTags = formData.get("lf_tags") || ""; + let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== ""); + + // Remove existing color tags (only one note-color-* allowed) + tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_COLOR_TAG_PREFIX)); + + // Backward compat cleanup: remove legacy note-custom- + tagsArray = tagsArray.filter((t) => !t.startsWith("note-custom-")); + + // Backward compat cleanup: remove legacy note- + tagsArray = tagsArray.filter((t) => { + if (!t.startsWith("note-")) return true; + const colorKey = t.substring(5); + return !NOTE_COLOR_OPTIONS.some((opt) => opt.key === colorKey); + }); + + // Add custom color tag + const cleanColor = color.replace("#", ""); + tagsArray.push(`${NOTE_COLOR_TAG_PREFIX}${cleanColor}`); + + formData.set("lf_tags", tagsArray.join(" ")); + formData.append("save_edit", "1"); + + return fetch(form.action, { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: formData.toString(), + }); + }) + .then((response) => { + if (!response.ok) throw new Error("Failed to save custom color"); + console.log(`Custom color ${color} saved for note ${currentNote.id}`); + }) + .catch((err) => { + console.error("Error saving custom color:", err); + }); +}