diff --git a/shaarli-pro/css/custom_views.css b/shaarli-pro/css/custom_views.css index ccdf911..6f7efdc 100644 --- a/shaarli-pro/css/custom_views.css +++ b/shaarli-pro/css/custom_views.css @@ -774,11 +774,16 @@ body.view-notes .content-container { background: var(--background-secondary, #ffffff); border: 1px solid rgba(0, 0, 0, 0.12); border-radius: 12px; - padding: 10px; - box-shadow: 0 10px 24px rgba(0, 0, 0, 0.18); + padding: 12px; + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.22); z-index: 50; width: max-content; max-width: min(340px, calc(100vw - 32px)); + max-height: min(320px, calc(100vh - 140px)); + overflow: auto; + overscroll-behavior: contain; + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); } [data-theme="dark"] .palette-popup { @@ -790,25 +795,84 @@ body.view-notes .content-container { display: block; } +.palette-section { + display: flex; + flex-direction: column; + gap: 10px; +} + +.palette-section-title { + font-size: 0.75rem; + line-height: 1; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgba(0, 0, 0, 0.62); +} + +[data-theme="dark"] .palette-section-title { + color: rgba(255, 255, 255, 0.72); +} + +.palette-divider { + height: 1px; + background: rgba(0, 0, 0, 0.10); + margin: 10px 0; +} + +[data-theme="dark"] .palette-divider { + background: rgba(255, 255, 255, 0.12); +} + .palette-row { display: flex; flex-wrap: wrap; gap: 8px; } +.palette-row-colors { + flex-wrap: nowrap; + overflow-x: hidden; + overflow-y: hidden; + padding-bottom: 4px; + scrollbar-width: thin; +} + +.palette-row-colors::-webkit-scrollbar { + height: 8px; +} + +.palette-row-colors::-webkit-scrollbar-thumb { + background: rgba(0, 0, 0, 0.18); + border-radius: 10px; +} + +[data-theme="dark"] .palette-row-colors::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); +} + +.palette-row-backgrounds { + display: grid; + grid-template-columns: repeat(auto-fill, 28px); + gap: 10px; + justify-content: start; +} + .palette-row + .palette-row { margin-top: 10px; } .palette-btn { - width: 24px; - height: 24px; + width: 28px; + height: 28px; border-radius: 50%; border: 1px solid rgba(0, 0, 0, 0.18); cursor: pointer; padding: 0; background-position: center bottom; background-size: cover; + flex: 0 0 auto; + transition: transform 0.12s ease, box-shadow 0.12s ease, border-color 0.12s ease; } [data-theme="dark"] .palette-btn { @@ -820,6 +884,20 @@ body.view-notes .content-container { outline-offset: 2px; } +.palette-btn:hover { + transform: translateY(-1px); + box-shadow: 0 6px 14px rgba(0, 0, 0, 0.18); +} + +[data-theme="dark"] .palette-btn:hover { + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.45); +} + +.palette-btn:focus-visible { + outline: 2px solid var(--primary-color, #2563eb); + outline-offset: 2px; +} + .palette-btn-bg-none { display: flex; align-items: center; @@ -835,6 +913,385 @@ body.view-notes .content-container { top: calc(100% + 8px); } +.bg-studio-panel { + position: fixed; + z-index: 1200; + width: min(420px, calc(100vw - 24px)); + border-radius: 20px; + background: rgba(255, 255, 255, 0.92); + border: 1px solid var(--border, #d1d5db); + box-shadow: 0 20px 50px rgba(15, 23, 42, 0.22); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + color: var(--text-main, #0f172a); + padding: 16px; +} + +[data-theme="dark"] .bg-studio-panel { + background: rgba(31, 37, 41, 0.92); + border-color: #2A3238; + box-shadow: 0 28px 70px rgba(0, 0, 0, 0.55); + color: rgba(255, 255, 255, 0.92); +} + +.bg-studio-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.bg-studio-title { + font-size: 18px; + font-weight: 600; + letter-spacing: 0.01em; +} + +.bg-studio-close { + width: 34px; + height: 34px; + border-radius: 12px; + 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; +} + +.bg-studio-close:hover { + background: rgba(15, 23, 42, 0.10); +} + +[data-theme="dark"] .bg-studio-close { + border-color: rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); +} + +[data-theme="dark"] .bg-studio-close:hover { + background: rgba(255, 255, 255, 0.10); +} + +.bg-studio-gallery { + display: grid; + grid-auto-flow: column; + grid-template-rows: repeat(2, 102px); + grid-auto-columns: 84px; + gap: 12px; + overflow-x: auto; + overflow-y: hidden; + padding: 2px 2px 10px; + margin-bottom: 12px; + overscroll-behavior: contain; + scrollbar-width: thin; +} + +.bg-studio-gallery::-webkit-scrollbar { + height: 10px; +} + +.bg-studio-gallery::-webkit-scrollbar-thumb { + background: rgba(15, 23, 42, 0.20); + border-radius: 10px; +} + +[data-theme="dark"] .bg-studio-gallery::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.18); +} + +.bg-studio-thumb { + width: 84px; + height: 72px; + border-radius: 12px; + border: 1px solid rgba(15, 23, 42, 0.16); + background: rgba(15, 23, 42, 0.05); + background-position: center bottom; + background-size: cover; + cursor: pointer; + padding: 0; + position: relative; + transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease; + flex: 0 0 auto; + overflow: hidden; + isolation: isolate; +} + +.bg-studio-thumb:hover { + transform: translateY(-2px); + box-shadow: 0 12px 30px rgba(15, 23, 42, 0.28); +} + +.bg-studio-thumb.is-active { + border: 3px solid #3b82f6; + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22); +} + +.bg-studio-thumb:focus-visible { + outline: 3px solid rgba(59, 130, 246, 0.9); + outline-offset: 3px; +} + +.bg-studio-thumb-color, +.bg-studio-thumb-solid { + position: absolute; + inset: 0; + border-radius: 12px; +} + +.bg-studio-thumb-solid { + background: linear-gradient(135deg, rgba(255,255,255,0.15), rgba(255,255,255,0.02)); +} + +.bg-studio-tile { + display: grid; + grid-template-rows: 72px auto; + gap: 6px; + width: 84px; +} + +.bg-studio-thumb-label { + font-size: 0.72rem; + line-height: 1.1; + text-align: center; + color: var(--text-muted, rgba(15, 23, 42, 0.62)); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +[data-theme="dark"] .bg-studio-thumb-label { + color: rgba(255, 255, 255, 0.72); +} + +.bg-studio-rows { + display: grid; + grid-template-columns: 1fr; + gap: 10px; + margin-bottom: 12px; +} + +.bg-studio-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr); + align-items: center; + gap: 12px; +} + +.bg-studio-row-heading { + display: inline-flex; + align-items: center; + gap: 8px; +} + +.bg-studio-row-label { + min-width: 108px; + font-size: 0.9rem; + font-weight: 600; + 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; + gap: 8px; + min-width: 0; + width: 100%; + overflow-x: auto; + overflow-y: hidden; +} + +.bg-studio-filter-btn { + width: 30px; + height: 30px; + border-radius: 8px; + 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; +} + +.bg-studio-filter-btn.is-active { + border-color: rgba(59, 130, 246, 0.9); + color: #60a5fa; +} + +[data-theme="dark"] .bg-studio-filter-btn { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.06); +} + +.bg-studio-reset { + 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; +} + +.bg-studio-reset:hover { + border-color: rgba(59, 130, 246, 0.5); + background: linear-gradient(180deg, #ffffff, #e9eefb); + box-shadow: 0 6px 14px rgba(59, 130, 246, 0.2); + transform: translateY(-1px); +} + +.bg-studio-reset:focus-visible { + outline: 2px solid rgba(59, 130, 246, 0.9); + outline-offset: 2px; +} + +[data-theme="dark"] .bg-studio-reset { + border-color: rgba(255, 255, 255, 0.2); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.16), rgba(255, 255, 255, 0.08)); + color: rgba(255, 255, 255, 0.92); + box-shadow: none; +} + +[data-theme="dark"] .bg-studio-reset:hover { + border-color: rgba(96, 165, 250, 0.65); + background: linear-gradient(180deg, rgba(96, 165, 250, 0.26), rgba(96, 165, 250, 0.16)); + box-shadow: 0 8px 16px rgba(2, 6, 23, 0.45); +} + +.bg-studio-search { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 10px 12px; + border-radius: 14px; + border: 1px solid var(--border, rgba(15, 23, 42, 0.16)); + background: rgba(15, 23, 42, 0.05); + margin-bottom: 12px; + transition: border-color 0.16s ease, box-shadow 0.16s ease, background-color 0.16s ease; +} + +.bg-studio-search:focus-within { + border-color: rgba(59, 130, 246, 0.55); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18); +} + +.bg-studio-search i { + color: var(--text-muted, rgba(15, 23, 42, 0.62)); +} + +.bg-studio-search-input { + flex: 1; + border: 0 !important; + background: transparent !important; + box-shadow: none !important; + border-radius: 0; + color: inherit; + outline: none; + font-size: 0.95rem; + -webkit-appearance: none; + appearance: none; + caret-color: currentColor; +} + +.bg-studio-search-input:focus, +.bg-studio-search-input:focus-visible { + outline: none; + box-shadow: none; +} + +.bg-studio-search-input::-webkit-search-cancel-button { + -webkit-appearance: none; + appearance: none; +} + +.bg-studio-search-input::placeholder { + color: var(--text-muted, rgba(15, 23, 42, 0.52)); +} + +[data-theme="dark"] .bg-studio-search { + border-color: rgba(255, 255, 255, 0.18); + background: rgba(255, 255, 255, 0.08); +} + +[data-theme="dark"] .bg-studio-search:focus-within { + border-color: rgba(96, 165, 250, 0.75); + background: rgba(15, 23, 42, 0.34); + box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.2); +} + +[data-theme="dark"] .bg-studio-search i { + color: rgba(255, 255, 255, 0.82); +} + +[data-theme="dark"] .bg-studio-search-input { + color: rgba(255, 255, 255, 0.95); +} + +[data-theme="dark"] .bg-studio-search-input::placeholder { + color: rgba(255, 255, 255, 0.62); +} + +.bg-studio-clear { + width: 28px; + height: 28px; + border-radius: 999px; + 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; +} + +[data-theme="dark"] .bg-studio-clear { + border-color: rgba(255, 255, 255, 0.16); + background: rgba(255, 255, 255, 0.06); +} + .spacer { flex: 1; } diff --git a/shaarli-pro/img/note-bg-dark/bg-dark-radio.jpg b/shaarli-pro/img/note-bg-dark/bg-dark-radio.jpg new file mode 100644 index 0000000..72894c3 Binary files /dev/null and b/shaarli-pro/img/note-bg-dark/bg-dark-radio.jpg differ diff --git a/shaarli-pro/img/note-bg-light/bg-light-radio.jpg b/shaarli-pro/img/note-bg-light/bg-light-radio.jpg new file mode 100644 index 0000000..ffa07dd Binary files /dev/null and b/shaarli-pro/img/note-bg-light/bg-light-radio.jpg differ diff --git a/shaarli-pro/js/backgrounds-manifest.js b/shaarli-pro/js/backgrounds-manifest.js index 55f4d0f..4d56b24 100644 --- a/shaarli-pro/js/backgrounds-manifest.js +++ b/shaarli-pro/js/backgrounds-manifest.js @@ -103,6 +103,14 @@ window.SHAARLI_NOTE_BACKGROUNDS_MANIFEST = [ dark: "bg-dark-ocean.jpg", }, }, + { + "key": "radio", + "label": "Radio", + "files": { + "light": "bg-light-radio.jpg", + "dark": "bg-dark-radio.jpg" + } + }, { key: "sports", label: "Sports", diff --git a/shaarli-pro/js/backgrounds-manifest.json b/shaarli-pro/js/backgrounds-manifest.json index 17536df..77a5755 100644 --- a/shaarli-pro/js/backgrounds-manifest.json +++ b/shaarli-pro/js/backgrounds-manifest.json @@ -103,6 +103,14 @@ "dark": "bg-dark-ocean.jpg" } }, + { + "key": "radio", + "label": "Radio", + "files": { + "light": "bg-light-radio.jpg", + "dark": "bg-dark-radio.jpg" + } + }, { "key": "sports", "label": "Sports", @@ -143,4 +151,5 @@ "dark": "bg-dark-voyage.jpg" } } + ] diff --git a/shaarli-pro/js/custom_views.js b/shaarli-pro/js/custom_views.js index 568b454..5df2183 100644 --- a/shaarli-pro/js/custom_views.js +++ b/shaarli-pro/js/custom_views.js @@ -173,6 +173,7 @@ const NOTE_BACKGROUND_MANIFEST_INLINE = [ { key: "legumes", label: "Légumes", files: { light: "bg-light-legumes.jpg", dark: "bg-dark-legumes.jpg" } }, { key: "montagnes", label: "Montagnes", files: { light: "bg-light-montagnes.jpg", dark: "bg-dark-montagnes.jpg" } }, { key: "ocean", label: "Océan", files: { light: "bg-light-ocean.jpg", dark: "bg-dark-ocean.jpg" } }, + { key: "radio", label: "Radio", files: { light: "bg-light-radio.jpg", dark: "bg-dark-radio.jpg" } }, { key: "sports", label: "Sports", files: { light: "bg-light-sports.jpg", dark: "bg-dark-sports.jpg" } }, { key: "vague1", label: "Vague 1", files: { light: "bg-light-vague1.jpg", dark: "bg-dark-vague1.jpg" } }, { key: "vague2", label: "Vague 2", files: { light: "bg-light-vague2.jpg", dark: "bg-dark-vague2.jpg" } }, @@ -499,6 +500,428 @@ function positionPalettePopup(popup) { } } +let backgroundStudioPanelInitialized = false; + +function ensureBackgroundStudioPanel() { + if (backgroundStudioPanelInitialized) return; + if (document.getElementById("shaarli-bg-studio")) { + backgroundStudioPanelInitialized = true; + return; + } + + const panel = document.createElement("div"); + panel.id = "shaarli-bg-studio"; + panel.className = "bg-studio-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-bg-studio"); + if (!p || !p.classList.contains("open")) return; + if (e.target.closest("#shaarli-bg-studio")) return; + const anchor = p.__anchorEl; + if (anchor && (e.target === anchor || e.target.closest(`#${anchor.id}`))) return; + closeBackgroundStudioPanel(); + }); + + document.addEventListener("keydown", (e) => { + if (e.key !== "Escape") return; + const p = document.getElementById("shaarli-bg-studio"); + if (!p || !p.classList.contains("open")) return; + closeBackgroundStudioPanel(); + }); + + panel.addEventListener("click", (e) => { + const actionBtn = e.target.closest("button[data-bg-studio-action]"); + if (!actionBtn) return; + + const panelEl = document.getElementById("shaarli-bg-studio"); + if (!panelEl) return; + + const mode = panelEl.dataset.mode || "entity"; + const entityId = panelEl.dataset.entityId || ""; + const editUrl = panelEl.dataset.editUrl || ""; + + const action = actionBtn.dataset.bgStudioAction; + if (action === "close") { + closeBackgroundStudioPanel(); + return; + } + + if (action === "set-color") { + const key = actionBtn.dataset.colorKey || "default"; + if (mode === "modal") setModalNoteColor(key); + else setNoteColor(entityId, key, editUrl); + return; + } + + if (action === "set-background") { + const key = actionBtn.dataset.bgKey || "none"; + if (mode === "modal") setModalNoteBackground(key); + else setNoteBackground(entityId, key, editUrl); + return; + } + + if (action === "set-filter") { + panelEl.dataset.filter = actionBtn.dataset.filter || "all"; + renderBackgroundStudioPanel(panelEl); + return; + } + + if (action === "set-query") { + panelEl.dataset.query = ""; + renderBackgroundStudioPanel(panelEl); + const input = panelEl.querySelector(".bg-studio-search-input"); + if (input) input.focus(); + return; + } + + if (action === "set-defaults") { + if (mode === "modal") { + setModalNoteColor("default"); + setModalNoteBackground("none"); + } else { + setNoteColor(entityId, "default", editUrl); + setNoteBackground(entityId, "none", editUrl); + } + return; + } + }); + + panel.addEventListener("keydown", (e) => { + const targetBtn = e.target && e.target.closest ? e.target.closest("button") : null; + if (!targetBtn) return; + + if (e.key === "Enter" || e.key === " ") { + if (targetBtn.hasAttribute("data-bg-studio-action") || targetBtn.classList.contains("bg-studio-thumb") || targetBtn.classList.contains("bg-studio-swatch")) { + e.preventDefault(); + targetBtn.click(); + } + return; + } + + const moveFocus = (buttons, nextIndex) => { + if (!buttons || buttons.length === 0) return; + const idx = Math.max(0, Math.min(buttons.length - 1, nextIndex)); + const el = buttons[idx]; + if (el && typeof el.focus === "function") el.focus(); + }; + + if (targetBtn.classList.contains("bg-studio-thumb")) { + const buttons = Array.from(panel.querySelectorAll(".bg-studio-gallery .bg-studio-thumb")); + const currentIndex = buttons.indexOf(targetBtn); + if (currentIndex === -1) return; + + const colSize = 2; + if (e.key === "ArrowRight") { + e.preventDefault(); + moveFocus(buttons, currentIndex + colSize); + } else if (e.key === "ArrowLeft") { + e.preventDefault(); + moveFocus(buttons, currentIndex - colSize); + } else if (e.key === "ArrowDown") { + e.preventDefault(); + moveFocus(buttons, currentIndex + 1); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + moveFocus(buttons, currentIndex - 1); + } + return; + } + + if (targetBtn.classList.contains("bg-studio-swatch")) { + const buttons = Array.from(panel.querySelectorAll(".bg-studio-swatches .bg-studio-swatch")); + const currentIndex = buttons.indexOf(targetBtn); + if (currentIndex === -1) return; + + if (e.key === "ArrowRight") { + e.preventDefault(); + moveFocus(buttons, currentIndex + 1); + } else if (e.key === "ArrowLeft") { + e.preventDefault(); + moveFocus(buttons, currentIndex - 1); + } + return; + } + }); + + panel.addEventListener("input", (e) => { + const input = e.target && e.target.matches(".bg-studio-search-input") ? e.target : null; + if (!input) return; + + const panelEl = document.getElementById("shaarli-bg-studio"); + if (!panelEl) return; + panelEl.dataset.query = input.value || ""; + + window.clearTimeout(panelEl.__searchTimer); + panelEl.__searchTimer = window.setTimeout(() => { + renderBackgroundStudioPanel(panelEl); + }, 150); + }); + + backgroundStudioPanelInitialized = true; +} + +function closeBackgroundStudioPanel() { + const panel = document.getElementById("shaarli-bg-studio"); + if (!panel) return; + panel.classList.remove("open"); + panel.style.display = "none"; + panel.setAttribute("aria-hidden", "true"); + panel.__anchorEl = null; +} + +function openBackgroundStudioPanel({ + anchorEl, + mode, + entityId, + editUrl, + currentColor, + currentBackground, + title, +}) { + ensureBackgroundStudioPanel(); + const panel = document.getElementById("shaarli-bg-studio"); + if (!panel) return; + + panel.dataset.mode = mode || "entity"; + panel.dataset.entityId = entityId || ""; + panel.dataset.editUrl = editUrl || ""; + panel.dataset.color = currentColor || "default"; + panel.dataset.background = normalizeBackgroundKey(currentBackground || "") || "none"; + panel.dataset.filter = panel.dataset.filter || "all"; + panel.dataset.query = panel.dataset.query || ""; + panel.dataset.title = title || "Mes images & couleurs"; + panel.__anchorEl = anchorEl || null; + + renderBackgroundStudioPanel(panel); + + panel.style.display = "block"; + panel.classList.add("open"); + panel.setAttribute("aria-hidden", "false"); + + positionBackgroundStudioPanel(panel, anchorEl); + + window.requestAnimationFrame(() => { + const input = panel.querySelector(".bg-studio-search-input"); + if (input && typeof input.focus === "function") { + input.focus(); + input.select(); + } + }); +} + +function positionBackgroundStudioPanel(panel, anchorEl) { + if (!panel) return; + + const viewportPadding = 12; + const rect = anchorEl && anchorEl.getBoundingClientRect ? anchorEl.getBoundingClientRect() : null; + + panel.style.left = ""; + panel.style.top = ""; + panel.style.right = ""; + panel.style.bottom = ""; + + const panelRect = panel.getBoundingClientRect(); + const preferredTop = rect ? rect.top - panelRect.height - 10 : viewportPadding; + const preferredLeft = rect ? rect.left : viewportPadding; + + let top = preferredTop; + if (top < viewportPadding) { + top = rect ? rect.bottom + 10 : viewportPadding; + } + + let left = preferredLeft; + if (left + panelRect.width > window.innerWidth - viewportPadding) { + left = window.innerWidth - viewportPadding - panelRect.width; + } + if (left < viewportPadding) left = viewportPadding; + + if (top + panelRect.height > window.innerHeight - viewportPadding) { + top = window.innerHeight - viewportPadding - panelRect.height; + } + if (top < viewportPadding) top = viewportPadding; + + panel.style.left = `${Math.round(left)}px`; + panel.style.top = `${Math.round(top)}px`; +} + +function normalizeSearchText(value) { + return String(value || "") + .toLowerCase() + .normalize("NFD") + .replace(/[\u0300-\u036f]/g, "") + .trim(); +} + +function categorizeBackgroundKey(key, label) { + const hay = normalizeSearchText(`${key || ""} ${label || ""}`); + + if (!key || key === "none") return "solid"; + if (hay.includes("degrade") || hay.includes("gradient")) return "gradient"; + if (hay.includes("grid") || hay.includes("quadrill") || hay.includes("lign") || hay.includes("feuille")) return "grid"; + if (hay.includes("damier") || hay.includes("checker") || hay.includes("texture")) return "texture"; + + return "image"; +} + +function getBackgroundStudioItems() { + const colors = NOTE_COLOR_OPTIONS.map((opt) => ({ + type: "color", + key: opt.key, + label: opt.label, + color: getThemeColorValue(opt), + keywords: [opt.key, opt.label], + })); + + const backgrounds = [ + { + type: "background", + category: "solid", + key: "none", + label: "Couleur unie", + keywords: ["none", "sans image", "solid", "uni"], + }, + ...getAvailableBackgroundOptionsForMode().map((bg) => ({ + type: "background", + category: categorizeBackgroundKey(bg.key, bg.label), + key: bg.key, + label: bg.label, + url: getNoteBackgroundUrl(bg.key), + keywords: [bg.key, bg.label, "image", "background"], + })), + ]; + + return { colors, backgrounds }; +} + +function renderBackgroundStudioPanel(panel) { + if (!panel) return; + + const activeEl = document.activeElement; + const wasSearchFocused = !!( + activeEl && + panel.contains(activeEl) && + activeEl.classList && + activeEl.classList.contains("bg-studio-search-input") + ); + const caretStart = wasSearchFocused && typeof activeEl.selectionStart === "number" ? activeEl.selectionStart : null; + const caretEnd = wasSearchFocused && typeof activeEl.selectionEnd === "number" ? activeEl.selectionEnd : null; + + const title = panel.dataset.title || "Mes images & couleurs"; + const color = panel.dataset.color || "default"; + const background = normalizeBackgroundKey(panel.dataset.background || "") || "none"; + const filter = panel.dataset.filter || "all"; + const query = normalizeSearchText(panel.dataset.query || ""); + + const { colors, backgrounds } = getBackgroundStudioItems(); + + const filterMatch = (item) => { + if (filter === "all") return true; + if (filter === "color") return item.type === "color"; + if (item.type !== "background") return false; + if (filter === "solid") return item.category === "solid"; + if (filter === "gradient") return item.category === "gradient"; + if (filter === "grid") return item.category === "grid"; + if (filter === "image") return item.category === "image"; + if (filter === "texture") return item.category === "texture"; + return true; + }; + + const queryMatch = (item) => { + if (!query) return true; + const hay = normalizeSearchText([item.label, ...(item.keywords || [])].join(" ")); + return hay.includes(query); + }; + + const galleryItems = backgrounds + .filter((b) => filterMatch(b) && queryMatch(b)) + .map((b) => ({ ...b, kind: "background" })); + + const galleryHtml = galleryItems + .map((item) => { + const isActive = background === item.key; + const common = `class="bg-studio-thumb ${isActive ? "is-active" : ""}" type="button"`; + const thumb = + item.key === "none" + ? `` + : ``; + + return `
${thumb}
${item.label}
`; + }) + .join(""); + + const colorsRowHtml = colors + .filter((c) => c.key !== "default") + .map((c) => { + const isActive = color === c.key; + const hex = c.color || ""; + return ``; + }) + .join(""); + + const filterBtn = (key, icon, label) => { + const active = filter === key; + return ``; + }; + + const filtersHtml = [ + filterBtn("solid", "mdi-checkbox-blank-outline", "Couleur unie"), + filterBtn("gradient", "mdi-gradient-horizontal", "Dégradé"), + filterBtn("grid", "mdi-grid", "Motif grille"), + filterBtn("image", "mdi-image-outline", "Image personnalisée"), + filterBtn("texture", "mdi-checkerboard", "Texture"), + ].join(""); + + const showClear = !!(panel.dataset.query || "").trim(); + + panel.innerHTML = ` +
+
${title}
+ +
+ +
+
+
+
Colors:
+ +
+
${colorsRowHtml}
+
+
+
Backgrounds:
+
${filtersHtml}
+
+
+ + `; + + if (wasSearchFocused) { + const input = panel.querySelector(".bg-studio-search-input"); + if (input && typeof input.focus === "function") { + input.focus({ preventScroll: true }); + if (typeof input.setSelectionRange === "function") { + const len = (input.value || "").length; + const start = typeof caretStart === "number" ? Math.max(0, Math.min(len, caretStart)) : len; + const end = typeof caretEnd === "number" ? Math.max(0, Math.min(len, caretEnd)) : len; + try { + input.setSelectionRange(start, end); + } catch (e) { + // Ignore selection errors for non-text inputs or unsupported browsers. + } + } + } + } +} + function applyNoteVisualState(element, note) { if (!element || !note) return; @@ -595,9 +1018,6 @@ function initBookmarkPaletteButtons() { wrapper.style.position = "relative"; wrapper.innerHTML = ` - `; const pinLink = Array.from(actions.querySelectorAll('a[href*="/pin"], a[aria-label*="Épingler"], a[title*="Épingler"]'))[0]; @@ -610,59 +1030,81 @@ function initBookmarkPaletteButtons() { } const paletteBtn = wrapper.querySelector(`#${paletteBtnId}`); - const palettePopup = wrapper.querySelector(`#popup-${paletteBtnId}`); - if (!paletteBtn || !palettePopup) return; + if (!paletteBtn) return; paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); - document.querySelectorAll(".palette-popup.open").forEach((p) => { - if (p !== palettePopup) { - p.classList.remove("open"); - const parentCard = p.closest(".link-outer"); - if (parentCard) parentCard.classList.remove("palette-open"); - } + openBackgroundStudioPanel({ + anchorEl: paletteBtn, + mode: "entity", + entityId: cardId, + editUrl, + currentColor: getElementVisualColor(card), + currentBackground: getElementVisualBackground(card), + title: "Mes images & couleurs", }); - const nextOpenState = !palettePopup.classList.contains("open"); - palettePopup.classList.toggle("open"); - card.classList.toggle("palette-open", nextOpenState); - positionPalettePopup(palettePopup); }); }); document.addEventListener("click", (e) => { if (e.target.closest(".bookmark-palette")) return; - document.querySelectorAll(".palette-popup.open").forEach((p) => p.classList.remove("open")); - document.querySelectorAll(".link-outer.palette-open").forEach((el) => el.classList.remove("palette-open")); + if (e.target.closest("#shaarli-bg-studio")) return; + closeBackgroundStudioPanel(); }); } function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentBackground) { + return generateUnifiedPaletteMenu({ + entityId: bookmarkId, + editUrl, + currentColor, + currentBackground, + mode: "entity", + }); +} + +function generateUnifiedPaletteMenu({ entityId, editUrl, currentColor, currentBackground, mode }) { const color = currentColor || "default"; const background = normalizeBackgroundKey(currentBackground || "") || "none"; + const onColorClick = + mode === "modal" + ? (c) => `setModalNoteColor('${c}')` + : (c) => `setNoteColor('${entityId}', '${c}', '${editUrl}')`; + + const onBackgroundClick = + mode === "modal" + ? (k) => `setModalNoteBackground('${k}')` + : (k) => `setNoteBackground('${entityId}', '${k}', '${editUrl}')`; + const colorButtons = [ - ``, - ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( - (opt) => { - const swatchColor = getThemeColorValue(opt); - return ``; - }, - ), + ``, + ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map((opt) => { + const swatchColor = getThemeColorValue(opt); + return ``; + }), ].join(""); const backgroundButtons = [ - ``, + ``, ...getAvailableBackgroundOptionsForMode().map((bg) => { const bgUrl = getNoteBackgroundUrl(bg.key); - return ``; + return ``; }), ].join(""); return ` -
${colorButtons}
-
${backgroundButtons}
+
+
Couleurs
+
${colorButtons}
+
+
+
+
Images
+
${backgroundButtons}
+
`; } @@ -1021,8 +1463,16 @@ function initNoteView(linkList, container) { modalColorBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); - modalColorPopup.classList.toggle("open"); - positionPalettePopup(modalColorPopup); + const modalCard = modalOverlay.querySelector(".note-modal"); + openBackgroundStudioPanel({ + anchorEl: modalColorBtn, + mode: "modal", + entityId: modalCard ? modalCard.dataset.noteId || "" : "", + editUrl: modalCard ? modalCard.dataset.editUrl || "" : "", + currentColor: modalCard ? getElementVisualColor(modalCard) : "default", + currentBackground: modalCard ? getElementVisualBackground(modalCard) : "none", + title: "Mes images & couleurs", + }); }); modalPinBtn.addEventListener("click", (e) => { @@ -1255,9 +1705,6 @@ function renderNotes(container, notes, viewMode) {
-
@@ -1270,17 +1717,18 @@ function renderNotes(container, notes, viewMode) { // Palette Toggle const paletteBtn = actions.querySelector(`#${paletteBtnId}`); - const palettePopup = actions.querySelector(`#popup-${paletteBtnId}`); - paletteBtn.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); - // Close others? - document.querySelectorAll(".palette-popup.open").forEach((p) => { - if (p !== palettePopup) p.classList.remove("open"); + openBackgroundStudioPanel({ + anchorEl: paletteBtn, + mode: "entity", + entityId: note.id, + editUrl: note.editUrl, + currentColor: getElementVisualColor(card), + currentBackground: getElementVisualBackground(card), + title: "Mes images & couleurs", }); - palettePopup.classList.toggle("open"); - positionPalettePopup(palettePopup); }); inner.appendChild(actions); @@ -1365,31 +1813,13 @@ function openNoteModal(note) { } function generateModalPaletteButtons(note) { - const currentColor = note.color || "default"; - const currentBackground = normalizeBackgroundKey(note.background || "") || "none"; - - const colorButtons = [ - ``, - ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( - (opt) => { - const swatchColor = getThemeColorValue(opt); - return ``; - }, - ), - ].join(""); - - const backgroundButtons = [ - ``, - ...getAvailableBackgroundOptionsForMode().map((bg) => { - const bgUrl = getNoteBackgroundUrl(bg.key); - return ``; - }), - ].join(""); - - return ` -
${colorButtons}
-
${backgroundButtons}
- `; + return generateUnifiedPaletteMenu({ + entityId: note && note.id ? note.id : "", + editUrl: note && note.editUrl ? note.editUrl : "", + currentColor: note && note.color ? note.color : "default", + currentBackground: note && note.background ? note.background : "none", + mode: "modal", + }); } window.setModalNoteColor = function (color) { @@ -1436,31 +1866,13 @@ window.setModalNoteBackground = function (backgroundKey) { }; function generatePaletteButtons(note) { - const currentColor = note.color || "default"; - const currentBackground = normalizeBackgroundKey(note.background || "") || "none"; - - const colorButtons = [ - ``, - ...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map( - (opt) => { - const swatchColor = getThemeColorValue(opt); - return ``; - }, - ), - ].join(""); - - const backgroundButtons = [ - ``, - ...getAvailableBackgroundOptionsForMode().map((bg) => { - const bgUrl = getNoteBackgroundUrl(bg.key); - return ``; - }), - ].join(""); - - return ` -
${colorButtons}
-
${backgroundButtons}
- `; + return generateUnifiedPaletteMenu({ + entityId: note && note.id ? note.id : "", + editUrl: note && note.editUrl ? note.editUrl : "", + currentColor: note && note.color ? note.color : "default", + currentBackground: note && note.background ? note.background : "none", + mode: "entity", + }); } window.setNoteColor = function (noteId, color, editUrl) {