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) {
+
+
+
${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 = `
+
+
+
+ `;
+
+ // 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);
+ });
+}