feat: refactorer le système de couleurs des notes avec palette unifiée pour thème clair/sombre, ajout de support pour images de fond, amélioration du popup de palette avec grille responsive et accessibilité, et correction du z-index et overflow pour permettre l'affichage correct des popups

This commit is contained in:
Bruno Charest 2026-02-16 13:01:44 -05:00
parent 1721032254
commit 8f937c016d
63 changed files with 1409 additions and 136 deletions

View File

@ -380,8 +380,9 @@ body.view-notes .content-container {
/* Prevent split */
position: relative;
transition: box-shadow 0.2s, transform 0.2s, background-color 0.2s;
overflow: hidden;
/* for cover image */
overflow: visible;
/* allow palette popup to escape card bounds */
color: var(--note-card-fg, #202124);
}
/* Dark Mode Card */
@ -408,6 +409,11 @@ body.view-notes .content-container {
object-fit: cover;
}
.note-cover {
overflow: hidden;
border-radius: 8px 8px 0 0;
}
/* Inner Content */
.note-inner {
padding: 12px 16px;
@ -483,6 +489,7 @@ body.view-notes .content-container {
transition: opacity 0.2s;
position: relative;
/* For palette popup */
z-index: 2;
}
/* Show actions on hover */
@ -497,8 +504,9 @@ body.view-notes .content-container {
}
}
.note-hover-actions button,
.note-hover-actions a {
.note-hover-actions > button,
.note-hover-actions > a,
.note-hover-actions > div > button {
background: none;
border: none;
width: 34px;
@ -515,19 +523,22 @@ body.view-notes .content-container {
text-decoration: none;
}
[data-theme="dark"] .note-hover-actions button,
[data-theme="dark"] .note-hover-actions a {
[data-theme="dark"] .note-hover-actions > button,
[data-theme="dark"] .note-hover-actions > a,
[data-theme="dark"] .note-hover-actions > div > button {
color: #9aa0a6;
}
.note-hover-actions button:hover,
.note-hover-actions a:hover {
.note-hover-actions > button:hover,
.note-hover-actions > a:hover,
.note-hover-actions > div > button:hover {
background-color: rgba(136, 136, 136, 0.2);
color: var(--text-color);
}
[data-theme="dark"] .note-hover-actions button:hover,
[data-theme="dark"] .note-hover-actions a:hover {
[data-theme="dark"] .note-hover-actions > button:hover,
[data-theme="dark"] .note-hover-actions > a:hover,
[data-theme="dark"] .note-hover-actions > div > button:hover {
color: #e8eaed;
}
@ -549,181 +560,497 @@ body.view-notes .content-container {
/* Reference: Keep Colors */
/* Default */
.note-card.note-color-default {
background-color: var(--background-secondary, #ffffff);
background-color: #20293a;
border-color: transparent;
--note-card-fg: #dbe7ff;
}
[data-theme="dark"] .note-card.note-color-default {
background-color: #202124;
border-color: #5f6368;
background-color: #20293a;
border-color: transparent;
--note-card-fg: #dbe7ff;
}
/* Red */
.note-card.note-color-red {
background-color: #f28b82;
border-color: transparent;
--note-card-fg: #2f1714;
}
[data-theme="dark"] .note-card.note-color-red {
background-color: #5c2b29;
border-color: #5c2b29;
background-color: #f28b82;
border-color: transparent;
--note-card-fg: #2f1714;
}
/* Orange */
.note-card.note-color-orange {
background-color: #fbbc04;
border-color: transparent;
--note-card-fg: #3d2a00;
}
[data-theme="dark"] .note-card.note-color-orange {
background-color: #614a19;
border-color: #614a19;
background-color: #fbbc04;
border-color: transparent;
--note-card-fg: #3d2a00;
}
/* Yellow */
.note-card.note-color-yellow {
background-color: #fff475;
border-color: transparent;
--note-card-fg: #383100;
}
[data-theme="dark"] .note-card.note-color-yellow {
background-color: #635d19;
border-color: #635d19;
background-color: #fff475;
border-color: transparent;
--note-card-fg: #383100;
}
/* Green */
.note-card.note-color-green {
background-color: #ccff90;
border-color: transparent;
--note-card-fg: #203400;
}
[data-theme="dark"] .note-card.note-color-green {
background-color: #345920;
border-color: #345920;
background-color: #ccff90;
border-color: transparent;
--note-card-fg: #203400;
}
/* Teal */
.note-card.note-color-teal {
background-color: #a7ffeb;
border-color: transparent;
--note-card-fg: #08342d;
}
[data-theme="dark"] .note-card.note-color-teal {
background-color: #16504b;
border-color: #16504b;
background-color: #a7ffeb;
border-color: transparent;
--note-card-fg: #08342d;
}
/* Blue */
.note-card.note-color-blue {
background-color: #cbf0f8;
border-color: transparent;
--note-card-fg: #113541;
}
[data-theme="dark"] .note-card.note-color-blue {
background-color: #2d555e;
border-color: #2d555e;
background-color: #cbf0f8;
border-color: transparent;
--note-card-fg: #113541;
}
/* Dark Blue */
.note-card.note-color-darkblue {
background-color: #aecbfa;
border-color: transparent;
--note-card-fg: #102645;
}
[data-theme="dark"] .note-card.note-color-darkblue {
background-color: #1e3a5f;
border-color: #1e3a5f;
background-color: #aecbfa;
border-color: transparent;
--note-card-fg: #102645;
}
/* Purple */
.note-card.note-color-purple {
background-color: #d7aefb;
border-color: transparent;
--note-card-fg: #2f1845;
}
[data-theme="dark"] .note-card.note-color-purple {
background-color: #42275e;
border-color: #42275e;
background-color: #d7aefb;
border-color: transparent;
--note-card-fg: #2f1845;
}
/* Pink */
.note-card.note-color-pink {
background-color: #fdcfe8;
border-color: transparent;
--note-card-fg: #4a1d34;
}
[data-theme="dark"] .note-card.note-color-pink {
background-color: #5b2245;
border-color: #5b2245;
background-color: #fdcfe8;
border-color: transparent;
--note-card-fg: #4a1d34;
}
/* Brown */
.note-card.note-color-brown {
background-color: #e6c9a8;
border-color: transparent;
--note-card-fg: #3c2714;
}
[data-theme="dark"] .note-card.note-color-brown {
background-color: #442f19;
border-color: #442f19;
background-color: #e6c9a8;
border-color: transparent;
--note-card-fg: #3c2714;
}
/* Grey */
.note-card.note-color-grey {
background-color: #e8eaed;
border-color: transparent;
--note-card-fg: #2a2d31;
}
[data-theme="dark"] .note-card.note-color-grey {
background-color: #3c3f43;
border-color: #3c3f43;
background-color: #e8eaed;
border-color: transparent;
--note-card-fg: #2a2d31;
}
.note-card.note-has-bg,
.note-modal.note-has-bg,
.link-outer.note-has-bg {
background-image: linear-gradient(rgba(0, 0, 0, 0.16), rgba(0, 0, 0, 0.16)), var(--note-bg-image);
background-size: cover;
background-position: center;
}
.note-card .note-title,
.note-card .note-body,
.note-card .note-tag,
.note-card .note-hover-actions button,
.note-card .note-hover-actions a {
color: inherit;
}
.note-card[class*="note-color-"] {
color: var(--note-card-fg, #202124);
}
.link-outer[class*="note-color-"] {
color: var(--note-card-fg, #202124);
}
.link-outer.note-color-default {
background-color: #20293a;
border-color: transparent;
--note-card-fg: #dbe7ff;
}
[data-theme="dark"] .link-outer.note-color-default {
background-color: #20293a;
border-color: transparent;
--note-card-fg: #dbe7ff;
}
.link-outer.note-color-red {
background-color: #f28b82;
border-color: transparent;
--note-card-fg: #2f1714;
}
[data-theme="dark"] .link-outer.note-color-red {
background-color: #f28b82;
border-color: transparent;
--note-card-fg: #2f1714;
}
.link-outer.note-color-orange {
background-color: #fbbc04;
border-color: transparent;
--note-card-fg: #3d2a00;
}
[data-theme="dark"] .link-outer.note-color-orange {
background-color: #fbbc04;
border-color: transparent;
--note-card-fg: #3d2a00;
}
.link-outer.note-color-yellow {
background-color: #fff475;
border-color: transparent;
--note-card-fg: #383100;
}
[data-theme="dark"] .link-outer.note-color-yellow {
background-color: #fff475;
border-color: transparent;
--note-card-fg: #383100;
}
.link-outer.note-color-green {
background-color: #ccff90;
border-color: transparent;
--note-card-fg: #203400;
}
[data-theme="dark"] .link-outer.note-color-green {
background-color: #ccff90;
border-color: transparent;
--note-card-fg: #203400;
}
.link-outer.note-color-teal {
background-color: #a7ffeb;
border-color: transparent;
--note-card-fg: #08342d;
}
[data-theme="dark"] .link-outer.note-color-teal {
background-color: #a7ffeb;
border-color: transparent;
--note-card-fg: #08342d;
}
.link-outer.note-color-blue {
background-color: #cbf0f8;
border-color: transparent;
--note-card-fg: #113541;
}
[data-theme="dark"] .link-outer.note-color-blue {
background-color: #cbf0f8;
border-color: transparent;
--note-card-fg: #113541;
}
.link-outer.note-color-darkblue {
background-color: #aecbfa;
border-color: transparent;
--note-card-fg: #102645;
}
[data-theme="dark"] .link-outer.note-color-darkblue {
background-color: #aecbfa;
border-color: transparent;
--note-card-fg: #102645;
}
.link-outer.note-color-purple {
background-color: #d7aefb;
border-color: transparent;
--note-card-fg: #2f1845;
}
[data-theme="dark"] .link-outer.note-color-purple {
background-color: #d7aefb;
border-color: transparent;
--note-card-fg: #2f1845;
}
.link-outer.note-color-pink {
background-color: #fdcfe8;
border-color: transparent;
--note-card-fg: #4a1d34;
}
[data-theme="dark"] .link-outer.note-color-pink {
background-color: #fdcfe8;
border-color: transparent;
--note-card-fg: #4a1d34;
}
.link-outer.note-color-brown {
background-color: #e6c9a8;
border-color: transparent;
--note-card-fg: #3c2714;
}
[data-theme="dark"] .link-outer.note-color-brown {
background-color: #e6c9a8;
border-color: transparent;
--note-card-fg: #3c2714;
}
.link-outer.note-color-grey {
background-color: #e8eaed;
border-color: transparent;
--note-card-fg: #2a2d31;
}
[data-theme="dark"] .link-outer.note-color-grey {
background-color: #e8eaed;
border-color: transparent;
--note-card-fg: #2a2d31;
}
.link-outer[class*="note-color-"] .link-title,
.link-outer[class*="note-color-"] .link-url,
.link-outer[class*="note-color-"] .link-meta,
.link-outer[class*="note-color-"] .link-description,
.link-outer[class*="note-color-"] .link-tag a,
.link-outer[class*="note-color-"] .link-actions a,
.link-outer[class*="note-color-"] .link-actions button,
.link-outer[class*="note-color-"] .bookmark-palette > button {
color: inherit;
}
.link-actions .bookmark-palette {
display: flex;
align-items: center;
}
.link-actions .bookmark-palette > button {
background: none;
border: none;
width: 36px;
height: 36px;
border-radius: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
}
.link-actions .bookmark-palette > button i {
font-size: 1.15rem;
}
.link-actions .bookmark-palette > button:hover {
background: rgba(255, 255, 255, 0.08);
}
.link-actions .bookmark-palette .palette-popup {
left: 0;
right: auto;
}
/* --- PALETTE POPUP --- */
.palette-popup {
position: absolute;
bottom: 100%;
left: 0;
width: 320px;
background: white;
width: min(500px, 92vw);
background: #1f232b;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
border-radius: 4px;
padding: 8px;
display: flex;
flex-wrap: wrap;
gap: 4px;
z-index: 100;
border-radius: 12px;
padding: 0;
z-index: 5000;
display: none;
/* JS toggles this */
}
[data-theme="dark"] .palette-popup {
background: #202124;
background: #1f232b;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
border: 1px solid #5f6368;
}
.palette-popup.open {
display: flex;
display: block;
}
.palette-popup.open-down {
top: calc(100% + 8px);
bottom: auto;
}
.palette-btn {
width: 24px;
height: 24px;
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
padding: 0;
border-radius: 50%;
border: 1px solid rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
position: relative;
flex: 0 0 auto;
background-position: center;
background-size: cover;
}
.palette-btn:hover {
border-color: #999;
border-color: rgba(255, 255, 255, 0.8);
}
[data-theme="dark"] .palette-btn {
border-color: rgba(255, 255, 255, 0.2);
}
.palette-btn.is-active {
outline: 2px solid #a855f7;
outline-offset: 1px;
}
.palette-row {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
overflow-x: auto;
flex-wrap: nowrap;
scrollbar-width: thin;
}
.palette-row::-webkit-scrollbar {
height: 6px;
}
.palette-row::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.24);
border-radius: 999px;
}
.palette-row+.palette-row {
border-top: 1px solid rgba(255, 255, 255, 0.14);
}
.palette-btn-default,
.palette-btn-bg-none {
background: #2b313c;
color: #d7dce5;
display: inline-flex;
align-items: center;
justify-content: center;
}
.palette-btn-default i,
.palette-btn-bg-none i {
font-size: 1rem;
}
/* Prevent link action generic button rules from deforming swatches in bookmark palette */
.link-actions .palette-popup .palette-btn {
width: 32px;
height: 32px;
min-width: 32px;
min-height: 32px;
border-radius: 50%;
padding: 0;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.18);
background-position: center;
background-size: cover;
}
.link-actions .palette-popup .palette-btn:hover {
background: transparent;
border-color: rgba(255, 255, 255, 0.8);
}
.link-outer.palette-open,
.view-list .link-outer.palette-open,
.view-compact .link-outer.palette-open {
overflow: visible;
z-index: 4000;
content-visibility: visible;
contain: none;
contain-intrinsic-size: auto;
}
/* --- MODAL FOR NOTE VIEW --- */
.note-modal-overlay {
position: fixed;
@ -748,40 +1075,301 @@ body.view-notes .content-container {
}
.note-modal {
background: var(--background-secondary, #fff);
width: 600px;
max-width: 90%;
max-height: 90vh;
border-radius: 8px;
overflow-y: auto;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
padding: 24px;
background: var(--modal-note-bg, #7a4b00);
color: var(--modal-note-fg, #fff2d9);
width: min(720px, 92vw);
max-height: 88vh;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.42);
position: relative;
display: flex;
flex-direction: column;
--note-scrollbar-thumb: rgba(255, 229, 170, 0.75);
--note-separator: rgba(255, 230, 182, 0.22);
--note-footer-bg: rgba(0, 0, 0, 0.14);
}
.note-modal.note-color-default {
--modal-note-bg: #20293a;
--modal-note-fg: #dbe7ff;
--note-scrollbar-thumb: rgba(219, 231, 255, 0.45);
--note-separator: rgba(219, 231, 255, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.08);
}
.note-modal.note-color-red {
--modal-note-bg: #f28b82;
--modal-note-fg: #2f1714;
--note-scrollbar-thumb: rgba(47, 23, 20, 0.45);
--note-separator: rgba(47, 23, 20, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.18);
}
.note-modal.note-color-orange {
--modal-note-bg: #fbbc04;
--modal-note-fg: #3d2a00;
--note-scrollbar-thumb: rgba(61, 42, 0, 0.45);
--note-separator: rgba(61, 42, 0, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
.note-modal.note-color-yellow {
--modal-note-bg: #fff475;
--modal-note-fg: #383100;
--note-scrollbar-thumb: rgba(56, 49, 0, 0.45);
--note-separator: rgba(56, 49, 0, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.24);
}
.note-modal.note-color-green {
--modal-note-bg: #ccff90;
--modal-note-fg: #203400;
--note-scrollbar-thumb: rgba(32, 52, 0, 0.45);
--note-separator: rgba(32, 52, 0, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.22);
}
.note-modal.note-color-teal {
--modal-note-bg: #a7ffeb;
--modal-note-fg: #08342d;
--note-scrollbar-thumb: rgba(8, 52, 45, 0.45);
--note-separator: rgba(8, 52, 45, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.22);
}
.note-modal.note-color-blue {
--modal-note-bg: #cbf0f8;
--modal-note-fg: #113541;
--note-scrollbar-thumb: rgba(17, 53, 65, 0.45);
--note-separator: rgba(17, 53, 65, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.22);
}
.note-modal.note-color-darkblue {
--modal-note-bg: #aecbfa;
--modal-note-fg: #102645;
--note-scrollbar-thumb: rgba(16, 38, 69, 0.45);
--note-separator: rgba(16, 38, 69, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
.note-modal.note-color-purple {
--modal-note-bg: #d7aefb;
--modal-note-fg: #2f1845;
--note-scrollbar-thumb: rgba(47, 24, 69, 0.45);
--note-separator: rgba(47, 24, 69, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
.note-modal.note-color-pink {
--modal-note-bg: #fdcfe8;
--modal-note-fg: #4a1d34;
--note-scrollbar-thumb: rgba(74, 29, 52, 0.45);
--note-separator: rgba(74, 29, 52, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
.note-modal.note-color-brown {
--modal-note-bg: #e6c9a8;
--modal-note-fg: #3c2714;
--note-scrollbar-thumb: rgba(60, 39, 20, 0.45);
--note-separator: rgba(60, 39, 20, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
.note-modal.note-color-grey {
--modal-note-bg: #e8eaed;
--modal-note-fg: #2a2d31;
--note-scrollbar-thumb: rgba(42, 45, 49, 0.45);
--note-separator: rgba(42, 45, 49, 0.2);
--note-footer-bg: rgba(255, 255, 255, 0.2);
}
[data-theme="dark"] .note-modal {
background: var(--bg-card);
border: 1px solid var(--border);
border: 1px solid rgba(255, 255, 255, 0.12);
}
.note-modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 18px 20px 10px;
flex-shrink: 0;
}
.note-modal .note-title {
font-size: 1.3rem;
line-height: 1.5;
margin-bottom: 16px;
font-size: 1.6rem;
line-height: 1.35;
margin: 0;
color: inherit;
font-weight: 500;
}
.note-modal-pin-toggle {
background: transparent;
border: none;
color: inherit;
opacity: 0.85;
width: 34px;
height: 34px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
transition: background-color 0.2s, opacity 0.2s;
}
.note-modal-pin-toggle i {
font-size: 1.2rem;
}
.note-modal-pin-toggle:hover,
.note-modal-pin-toggle.active {
opacity: 1;
background-color: rgba(255, 255, 255, 0.14);
}
.note-modal-content {
flex: 1;
overflow-y: auto;
padding: 0 20px 18px;
scrollbar-width: thin;
scrollbar-color: var(--note-scrollbar-thumb) transparent;
}
.note-modal-content::-webkit-scrollbar {
width: 10px;
}
.note-modal-content::-webkit-scrollbar-track {
background: transparent;
}
.note-modal-content::-webkit-scrollbar-thumb {
background: var(--note-scrollbar-thumb);
border-radius: 10px;
border: 2px solid transparent;
background-clip: content-box;
}
.note-modal .note-body {
font-size: 1rem;
line-height: 1.5;
-webkit-line-clamp: unset;
line-clamp: unset;
line-height: 1.75;
display: block;
-webkit-line-clamp: initial;
line-clamp: initial;
-webkit-box-orient: initial;
max-height: none;
overflow: visible;
color: inherit;
}
/* Modal actions at bottom? */
.note-modal-actions {
margin-top: 24px;
display: flex;
justify-content: flex-end;
.note-modal .note-body * {
color: inherit;
}
.note-modal-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 10px 20px 12px;
border-top: 1px solid var(--note-separator);
flex-shrink: 0;
}
.note-modal-tags.is-empty {
display: none;
}
.note-modal-tags .note-tag {
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.22);
color: inherit;
border-radius: 999px;
padding: 4px 10px;
font-size: 0.72rem;
letter-spacing: 0.02em;
}
.note-modal-actions {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 12px;
border-top: 1px solid var(--note-separator);
background: var(--note-footer-bg);
flex-shrink: 0;
}
.note-modal-actions-left {
display: flex;
align-items: center;
gap: 2px;
flex-wrap: wrap;
}
.note-modal-actions-left > button,
.note-modal-actions-left > a,
.note-modal-actions-left > .note-modal-color-picker > button {
background: transparent;
border: none;
color: inherit;
opacity: 0.9;
width: 32px;
height: 32px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
text-decoration: none;
transition: background-color 0.2s, opacity 0.2s;
}
.note-modal-actions-left > button:hover,
.note-modal-actions-left > a:hover,
.note-modal-actions-left > .note-modal-color-picker > button:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.14);
}
.note-modal-color-picker {
position: relative;
display: inline-flex;
}
.note-modal-palette {
left: -8px;
bottom: calc(100% + 8px);
width: min(500px, 92vw);
}
.note-modal-close-btn {
background: transparent;
border: none;
color: inherit;
font-weight: 600;
border-radius: 8px;
padding: 6px 10px;
cursor: pointer;
}
.note-modal-close-btn:hover {
background: rgba(255, 255, 255, 0.14);
}
@media (max-width: 640px) {
.note-modal {
width: 96vw;
max-height: 92vh;
}
.note-modal .note-title {
font-size: 1.25rem;
}
}

BIN
shaarli-pro/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,5 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="12" fill="#2563eb"/>
<path d="M21 17h22a4 4 0 0 1 4 4v26a4 4 0 0 1-4 4H21a4 4 0 0 1-4-4V21a4 4 0 0 1 4-4Z" fill="#ffffff"/>
<circle cx="32" cy="32" r="7" fill="#2563eb"/>
</svg>

Before

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

View File

@ -0,0 +1,34 @@
# Note backgrounds (Shaarli Pro)
Ce dossier contient les images de fond utilisées par la palette des notes (`notebg-*`).
## Fichiers attendus
Le thème référence actuellement ces fichiers :
- `bg-canyon.png`
- `bg-leaves.png`
- `bg-shore.png`
- `bg-sunset.png`
- `bg-planet.png`
- `bg-crystal.png`
- `bg-orchid.png`
- `bg-lake.png`
- `bg-ladder.png`
- `bg-burst.png`
## Formats recommandés
- **SVG** (idéal pour motifs/illustrations légères)
- **JPG / JPEG** (photos)
- **PNG** (illustrations avec transparence)
- **WEBP** (poids réduit, recommandé)
## Conseils d'image
- Ratio conseillé: **16:9** ou **4:3**
- Taille conseillée: entre **1200x800** et **1920x1080**
- Poids conseillé: **< 500 KB** par image pour garder l'interface fluide
- Privilégier des visuels **peu chargés** pour conserver la lisibilité du texte
Si vous remplacez une image, conservez le même nom de fichier pour qu'elle apparaisse automatiquement dans la palette.

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

View File

@ -2,7 +2,7 @@
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="same-origin">
<link rel="icon" type="image/svg+xml" href="/{function="ltrim($asset_path, '/')"}/img/favicon.svg#">
<link rel="icon" type="image/png" href="{$base_path}/{function="ltrim($asset_path, '/')"}/img/favicon.png">
<link rel="alternate" type="application/rss+xml" href="{$feedurl}?do=rss{$searchcrits}#" title="RSS Feed" />
<link rel="alternate" type="application/atom+xml" href="{$feedurl}?do=atom{$searchcrits}#" title="ATOM Feed" />
<link rel="search" type="application/opensearchdescription+xml" href="{$base_path}/open-search#" title="Shaarli search - {$shaarlititle}" />
@ -23,11 +23,11 @@
</script>
<!-- Professional Theme CSS -->
<link type="text/css" rel="stylesheet" href="/{function="ltrim($asset_path, '/')"}/css/style.css?v=1.0.1" />
<link type="text/css" rel="stylesheet" href="/{function="ltrim($asset_path, '/')"}/css/custom_views.css?v=1.0.1" />
<link type="text/css" rel="stylesheet" href="{$base_path}/{function="ltrim($asset_path, '/')"}/css/style.css?v=1.0.4" />
<link type="text/css" rel="stylesheet" href="{$base_path}/{function="ltrim($asset_path, '/')"}/css/custom_views.css?v=1.0.4" />
{if="$pageName=='editlink' || $pageName=='addlink' || $pageName=='editlinkbatch'"}
<link type="text/css" rel="stylesheet" href="/{function="ltrim($asset_path, '/')"}/css/awesomplete.css?v=1.0.1" />
<script src="/{function="ltrim($asset_path, '/')"}/js/awesomplete.min.js?v=1.0.1" defer></script>
<link type="text/css" rel="stylesheet" href="{$base_path}/{function="ltrim($asset_path, '/')"}/css/awesomplete.css?v=1.0.4" />
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/awesomplete.min.js?v=1.0.4" defer></script>
{/if}
<!-- Preconnect to external domains for performance -->
@ -43,7 +43,7 @@
<!-- Plugins CSS -->
{loop="$plugins_includes.css_files"}
<link type="text/css" rel="stylesheet" href="/{function="ltrim($value, '/')"}?v=1.0.1" />
<link type="text/css" rel="stylesheet" href="{$base_path}/{function="ltrim($value, '/')"}?v=1.0.1" />
{/loop}
{if="is_file('data/user.css')"}
@ -54,15 +54,15 @@
var shaarli = {
basePath: '{$base_path}',
rootPath: '{$root_path}',
assetPath: '/{function="ltrim($asset_path, '/')"}',
assetPath: '{$base_path}{$asset_path}',
isAuth: {if="$is_logged_in"}true{else}false{/if},
pageName: '{$pageName}',
visibility: '{$visibility}',
untaggedonly: {if="$untaggedonly"}true{else}false{/if}
};
</script>
<script src="/{function="ltrim($asset_path, '/')"}/js/script.js?v=1.0.1" defer></script>
<script src="/{function="ltrim($asset_path, '/')"}/js/custom_views.js?v=1.0.1" defer></script>
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/script.js?v=1.0.4" defer></script>
<script src="{$base_path}/{function="ltrim($asset_path, '/')"}/js/custom_views.js?v=1.0.4" defer></script>
{if="file_exists('tpl/shaarli-pro/extra.html')"}
{include="extra"}

View File

@ -6,6 +6,10 @@ document.addEventListener("DOMContentLoaded", function () {
const linkList = document.getElementById("links-list");
const container = document.querySelector(".content-container");
if (typeof initBookmarkPaletteButtons === "function") {
initBookmarkPaletteButtons();
}
// Always init Pinned Items logic (sorting and listeners)
// This function is defined at the end of the file
if (typeof initPinnedItems === "function") {
@ -21,6 +25,263 @@ document.addEventListener("DOMContentLoaded", function () {
}
});
const NOTE_COLOR_OPTIONS = [
{ key: "default", label: "Par défaut", hex: "#20293A" },
{ key: "red", label: "Rouge", hex: "#f28b82" },
{ key: "orange", label: "Orange", hex: "#fbbc04" },
{ key: "yellow", label: "Jaune", hex: "#fff475" },
{ key: "green", label: "Vert", hex: "#ccff90" },
{ key: "teal", label: "Menthe", hex: "#a7ffeb" },
{ key: "blue", label: "Bleu clair", hex: "#cbf0f8" },
{ key: "darkblue", label: "Bleu", hex: "#aecbfa" },
{ key: "purple", label: "Violet", hex: "#d7aefb" },
{ key: "pink", label: "Rose", hex: "#fdcfe8" },
{ key: "brown", label: "Beige", hex: "#e6c9a8" },
{ key: "grey", label: "Gris", hex: "#e8eaed" },
];
const NOTE_BACKGROUND_TAG_PREFIX = "notebg-";
function resolveThemeAssetBasePath() {
const cssLink = Array.from(document.querySelectorAll('link[rel="stylesheet"]')).find(
(link) => link.href && link.href.includes("/custom_views.css"),
);
if (cssLink && cssLink.href) {
const cssUrl = new URL(cssLink.href, window.location.origin);
const cssPath = cssUrl.pathname.replace(/\/css\/custom_views\.css$/, "");
if (cssPath) return cssPath;
}
const jsScript = Array.from(document.querySelectorAll("script[src]")).find(
(script) => script.src && script.src.includes("/custom_views.js"),
);
if (jsScript && jsScript.src) {
const jsUrl = new URL(jsScript.src, window.location.origin);
const jsPath = jsUrl.pathname.replace(/\/js\/custom_views\.js$/, "");
if (jsPath) return jsPath;
}
if (window.shaarli && typeof window.shaarli.assetPath === "string" && window.shaarli.assetPath.trim() !== "") {
return window.shaarli.assetPath.replace(/\/$/, "");
}
return "/tpl/shaarli-pro";
}
const NOTE_BACKGROUND_BASE_PATH = `${resolveThemeAssetBasePath().replace(/\/$/, "")}/img/note-backgrounds`;
const NOTE_BACKGROUND_OPTIONS = [
{ key: "bg-canyon", label: "Canyon", file: "bg-canyon.png" },
{ key: "bg-leaves", label: "Feuilles", file: "bg-leaves.png" },
{ key: "bg-shore", label: "Rivage", file: "bg-shore.png" },
{ key: "bg-sunset", label: "Coucher", file: "bg-sunset.png" },
{ key: "bg-planet", label: "Planète", file: "bg-planet.png" },
{ key: "bg-crystal", label: "Cristal", file: "bg-crystal.png" },
{ key: "bg-orchid", label: "Orchidée", file: "bg-orchid.png" },
{ key: "bg-lake", label: "Lac", file: "bg-lake.png" },
{ key: "bg-ladder", label: "Échelle", file: "bg-ladder.png" },
{ key: "bg-burst", label: "Étoile", file: "bg-burst.png" },
];
function getNoteBackgroundUrl(backgroundKey) {
const found = NOTE_BACKGROUND_OPTIONS.find((bg) => bg.key === backgroundKey);
if (!found) return "";
return `${NOTE_BACKGROUND_BASE_PATH}/${found.file}`;
}
function positionPalettePopup(popup) {
if (!popup || !popup.classList.contains("open")) return;
popup.classList.remove("open-down");
popup.style.left = "";
popup.style.right = "";
const viewportPadding = 8;
const upRect = popup.getBoundingClientRect();
if (upRect.top < viewportPadding) {
popup.classList.add("open-down");
const downRect = popup.getBoundingClientRect();
if (downRect.bottom > window.innerHeight - viewportPadding) {
popup.classList.remove("open-down");
}
}
let rect = popup.getBoundingClientRect();
if (rect.right > window.innerWidth - viewportPadding) {
popup.style.left = "auto";
popup.style.right = "0";
rect = popup.getBoundingClientRect();
}
if (rect.left < viewportPadding) {
popup.style.left = "0";
popup.style.right = "auto";
}
}
function applyNoteVisualState(element, note) {
if (!element || !note) return;
const color = note.color || "default";
const background = note.background || "none";
element.classList.forEach((cls) => {
if (cls.startsWith("note-color-")) element.classList.remove(cls);
});
element.classList.add(`note-color-${color}`);
if (background && background !== "none") {
const bgUrl = getNoteBackgroundUrl(background);
if (bgUrl) {
element.classList.add("note-has-bg");
element.style.setProperty("--note-bg-image", `url('${bgUrl}')`);
element.dataset.background = background;
} else {
element.classList.remove("note-has-bg");
element.style.removeProperty("--note-bg-image");
element.dataset.background = "none";
}
} else {
element.classList.remove("note-has-bg");
element.style.removeProperty("--note-bg-image");
element.dataset.background = "none";
}
element.dataset.color = color;
}
function extractNoteVisualStateFromTags(tags) {
const safeTags = Array.isArray(tags) ? tags : [];
let color = "default";
const foundColorTag = safeTags.find((t) => typeof t === "string" && t.startsWith("note-"));
if (foundColorTag) {
const candidate = foundColorTag.substring(5);
if (NOTE_COLOR_OPTIONS.some((opt) => opt.key === candidate)) {
color = candidate;
}
}
let background = "none";
const foundBgTag = safeTags.find((t) => typeof t === "string" && t.startsWith(NOTE_BACKGROUND_TAG_PREFIX));
if (foundBgTag) {
const candidate = foundBgTag.substring(NOTE_BACKGROUND_TAG_PREFIX.length);
if (candidate && NOTE_BACKGROUND_OPTIONS.some((bg) => bg.key === candidate)) {
background = candidate;
}
}
return { color, background };
}
function initBookmarkPaletteButtons() {
const linkCards = Array.from(document.querySelectorAll(".link-outer"));
if (linkCards.length === 0) return;
linkCards.forEach((card) => {
const cardId = card.dataset.id || card.getAttribute("data-id") || card.id;
if (!cardId) return;
const tags = Array.from(card.querySelectorAll(".link-tag a")).map((a) => (a.textContent || "").trim());
const { color, background } = extractNoteVisualStateFromTags(tags);
applyNoteVisualState(card, { color, background });
const actions = card.querySelector(".link-actions");
if (!actions) return;
if (actions.querySelector(".bookmark-palette")) return;
const editLink = actions.querySelector('a[title="Modifier"], a[aria-label="Modifier ce bookmark"], a[href*="/admin/shaare/"]');
if (!editLink || !editLink.href) return;
const editUrl = editLink.href;
const paletteBtnId = `bookmark-palette-${cardId}`;
const wrapper = document.createElement("div");
wrapper.className = "bookmark-palette";
wrapper.style.position = "relative";
wrapper.innerHTML = `
<button type="button" title="Couleur" aria-label="Couleur" id="${paletteBtnId}"><i class="mdi mdi-palette-outline" aria-hidden="true"></i></button>
<div class="palette-popup" id="popup-${paletteBtnId}">
${generateBookmarkPaletteButtons(cardId, editUrl, color, background)}
</div>
`;
const pinLink = Array.from(actions.querySelectorAll('a[href*="/pin"], a[aria-label*="Épingler"], a[title*="Épingler"]'))[0];
if (pinLink && pinLink.parentNode === actions) {
actions.insertBefore(wrapper, pinLink);
} else if (editLink && editLink.parentNode === actions) {
actions.insertBefore(wrapper, editLink.nextSibling);
} else {
actions.appendChild(wrapper);
}
const paletteBtn = wrapper.querySelector(`#${paletteBtnId}`);
const palettePopup = wrapper.querySelector(`#popup-${paletteBtnId}`);
if (!paletteBtn || !palettePopup) 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");
}
});
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"));
});
}
function generateBookmarkPaletteButtons(bookmarkId, editUrl, currentColor, currentBackground) {
const color = currentColor || "default";
const background = currentBackground || "none";
const colorButtons = [
`<button class="palette-btn palette-btn-default ${color === "default" ? "is-active" : ""}" title="Par défaut" onclick="setNoteColor('${bookmarkId}', 'default', '${editUrl}')"><i class="mdi mdi-format-color-reset"></i></button>`,
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
(opt) =>
`<button class="palette-btn note-color-${opt.key} ${color === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setNoteColor('${bookmarkId}', '${opt.key}', '${editUrl}')" style="background-color:${opt.hex}"></button>`,
),
].join("");
const backgroundButtons = [
`<button class="palette-btn palette-btn-bg-none ${background === "none" ? "is-active" : ""}" title="Sans image" onclick="setNoteBackground('${bookmarkId}', 'none', '${editUrl}')"><i class="mdi mdi-image-off-outline"></i></button>`,
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
const bgUrl = getNoteBackgroundUrl(bg.key);
return `<button class="palette-btn palette-btn-bg ${background === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setNoteBackground('${bookmarkId}', '${bg.key}', '${editUrl}')" style="background-image:url('${bgUrl}')"></button>`;
}),
].join("");
return `
<div class="palette-row palette-row-colors">${colorButtons}</div>
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
`;
}
function syncNoteFromCardElement(note, card) {
if (!note || !card) return;
const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-"));
if (colorClass) {
note.color = colorClass.replace("note-color-", "") || "default";
}
const background = card.dataset.background;
note.background = background && background !== "none" ? background : "none";
}
/**
* Initialize the Google Tasks-like view
*/
@ -305,10 +566,29 @@ function initNoteView(linkList, container) {
const modalOverlay = document.createElement("div");
modalOverlay.className = "note-modal-overlay";
modalOverlay.innerHTML = `
<div class="note-modal">
<div class="note-modal note-color-default">
<div class="note-modal-header">
<h2 class="note-title" id="note-modal-title"></h2>
<button type="button" class="note-modal-pin-toggle" id="note-modal-pin" title="Épingler">
<i class="mdi mdi-pin-outline"></i>
</button>
</div>
<div class="note-modal-content"></div>
<div class="note-modal-tags is-empty" id="note-modal-tags"></div>
<div class="note-modal-actions">
<button class="btn btn-primary" id="note-modal-close">Fermer</button>
<div class="note-modal-actions-left">
<div class="note-modal-color-picker">
<button type="button" id="note-modal-color-btn" title="Couleur"><i class="mdi mdi-palette-outline"></i></button>
<div class="palette-popup note-modal-palette" id="note-modal-color-popup"></div>
</div>
<button type="button" title="Rappel"><i class="mdi mdi-bell-outline"></i></button>
<button type="button" title="Collaborateur"><i class="mdi mdi-account-plus-outline"></i></button>
<button type="button" title="Image"><i class="mdi mdi-image-outline"></i></button>
<button type="button" id="note-modal-archive" title="Archiver"><i class="mdi mdi-archive-arrow-down-outline"></i></button>
<a href="#" id="note-modal-edit" title="Modifier"><i class="mdi mdi-pencil-outline"></i></a>
<button type="button" id="note-modal-delete" title="Supprimer"><i class="mdi mdi-dots-vertical"></i></button>
</div>
<button type="button" class="note-modal-close-btn" id="note-modal-close">Fermer</button>
</div>
</div>
`;
@ -337,6 +617,103 @@ function initNoteView(linkList, container) {
modalOverlay.addEventListener("click", (e) => {
if (e.target === modalOverlay) modalOverlay.classList.remove("open");
});
const modalPinBtn = modalOverlay.querySelector("#note-modal-pin");
const modalColorBtn = modalOverlay.querySelector("#note-modal-color-btn");
const modalColorPopup = modalOverlay.querySelector("#note-modal-color-popup");
modalColorBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
modalColorPopup.classList.toggle("open");
positionPalettePopup(modalColorPopup);
});
modalPinBtn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
const modalCard = modalOverlay.querySelector(".note-modal");
const noteId = modalCard.dataset.noteId;
const editUrl = modalCard.dataset.editUrl;
if (!noteId || !editUrl) return;
togglePinTag(noteId, editUrl, modalPinBtn);
const isPinned = modalPinBtn.classList.contains("active");
let tags = (modalCard.dataset.tags || "")
.split("||")
.filter((t) => t);
if (isPinned) {
if (!tags.includes("shaarli-pin")) tags.push("shaarli-pin");
} else {
tags = tags.filter((t) => t !== "shaarli-pin");
}
modalCard.dataset.tags = tags.join("||");
renderModalTags(modalOverlay.querySelector("#note-modal-tags"), tags);
if (modalOverlay.currentNote) {
modalOverlay.currentNote.isPinned = isPinned;
modalOverlay.currentNote.tags = tags;
}
});
modalOverlay.querySelector("#note-modal-delete").addEventListener("click", () => {
const modalCard = modalOverlay.querySelector(".note-modal");
const deleteUrl = modalCard.dataset.deleteUrl;
if (deleteUrl && deleteUrl !== "#") {
window.location.href = deleteUrl;
}
});
modalOverlay.querySelector("#note-modal-archive").addEventListener("click", (e) => {
e.preventDefault();
const modalCard = modalOverlay.querySelector(".note-modal");
const noteId = modalCard.dataset.noteId;
const editUrl = modalCard.dataset.editUrl;
if (!noteId || !editUrl) return;
addTagToNote(editUrl, "shaarli-archiver")
.then(() => {
if (modalOverlay.currentNote) {
if (!modalOverlay.currentNote.tags.includes("shaarli-archiver")) {
modalOverlay.currentNote.tags.push("shaarli-archiver");
}
}
const noteCard = document.querySelector(`.note-card[data-id="${noteId}"]`);
if (noteCard) noteCard.remove();
const index = notes.findIndex((n) => String(n.id) === String(noteId));
if (index > -1) notes.splice(index, 1);
modalOverlay.classList.remove("open");
})
.catch((err) => {
console.error("Error archiving note:", err);
alert("Erreur lors de l'archivage de la note.");
});
});
modalOverlay.addEventListener("click", (e) => {
if (!e.target.closest(".note-modal-color-picker")) {
modalColorPopup.classList.remove("open");
}
});
document.addEventListener("click", (e) => {
if (e.target.closest(".note-hover-actions .palette-popup") || e.target.closest('.note-hover-actions [id^="palette-"]')) {
return;
}
document.querySelectorAll(".note-hover-actions .palette-popup.open").forEach((p) => {
p.classList.remove("open");
});
});
}
function parseNoteFromLink(linkEl) {
@ -364,15 +741,23 @@ function parseNoteFromLink(linkEl) {
const tags = [];
let color = "default";
let background = "none";
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_BACKGROUND_TAG_PREFIX)) {
background = t.substring(NOTE_BACKGROUND_TAG_PREFIX.length) || "none";
} else {
tags.push(t);
}
});
const isPinnedByTag = tags.includes("shaarli-pin");
@ -385,30 +770,34 @@ 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, editUrl, deleteUrl, pinUrl, isPinned };
return { id, title, descHtml, descText, coverImage, url, tags, color, background, editUrl, deleteUrl, pinUrl, isPinned };
}
function renderNotes(container, notes, viewMode) {
container.innerHTML = "";
container.className = viewMode === "grid" ? "notes-masonry" : "notes-list-view";
const visibleNotes = notes.filter((note) => !(note.tags || []).includes("shaarli-archiver"));
// Sort: Pinned items first
notes.sort((a, b) => {
visibleNotes.sort((a, b) => {
const aPinned = a.tags.includes("shaarli-pin");
const bPinned = b.tags.includes("shaarli-pin");
return bPinned - aPinned;
});
notes.forEach((note) => {
visibleNotes.forEach((note) => {
const card = document.createElement("div");
card.className = `note-card note-color-${note.color}`;
card.className = "note-card";
card.dataset.id = note.id;
applyNoteVisualState(card, note);
if (viewMode === "list") card.classList.add("list-mode");
// Main Click to Open Modal
card.addEventListener("click", (e) => {
// Prevent if clicking buttons
if (e.target.closest("button") || e.target.closest("a") || e.target.closest(".note-hover-actions")) return;
syncNoteFromCardElement(note, card);
openNoteModal(note);
});
@ -488,18 +877,14 @@ function renderNotes(container, notes, viewMode) {
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");
});
palettePopup.classList.toggle("open");
});
// Close palette when clicking outside
// (Handled globally or card based? simple: card mouseleave?)
card.addEventListener("mouseleave", () => {
palettePopup.classList.remove("open");
positionPalettePopup(palettePopup);
});
inner.appendChild(actions);
@ -509,60 +894,203 @@ function renderNotes(container, notes, viewMode) {
});
}
function setModalPinButtonState(button, isPinned) {
if (!button) return;
button.classList.toggle("active", isPinned);
button.title = isPinned ? "Désépingler" : "Épingler";
const icon = button.querySelector("i");
if (icon) {
icon.className = `mdi ${isPinned ? "mdi-pin" : "mdi-pin-outline"}`;
}
}
function renderModalTags(container, tags) {
if (!container) return;
container.innerHTML = "";
const visibleTags = (tags || []).filter((tag) => tag && tag !== "note");
if (visibleTags.length === 0) {
container.classList.add("is-empty");
return;
}
container.classList.remove("is-empty");
visibleTags.forEach((tag) => {
const tagEl = document.createElement("span");
tagEl.className = "note-tag";
tagEl.textContent = tag;
container.appendChild(tagEl);
});
}
function openNoteModal(note) {
const modal = document.querySelector(".note-modal-overlay");
if (!modal) return;
const modalCard = modal.querySelector(".note-modal");
const title = modal.querySelector("#note-modal-title");
const content = modal.querySelector(".note-modal-content");
const tagsContainer = modal.querySelector("#note-modal-tags");
const editLink = modal.querySelector("#note-modal-edit");
const pinButton = modal.querySelector("#note-modal-pin");
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
// Build full content
let html = `
<h2 class="note-title" style="margin-top:0;">${note.title}</h2>
<div class="note-body">${note.descHtml}</div>
`;
// Add images if not in desc? (desc usually has it)
modal.currentNote = note;
modalCard.className = "note-modal";
applyNoteVisualState(modalCard, note);
modalCard.dataset.noteId = note.id || "";
modalCard.dataset.editUrl = note.editUrl || "";
modalCard.dataset.deleteUrl = note.deleteUrl || "";
modalCard.dataset.background = note.background || "none";
const visibleTags = (note.tags || []).filter((tag) => tag && tag !== "note");
modalCard.dataset.tags = visibleTags.join("||");
title.textContent = note.title || "Sans titre";
content.innerHTML = `<div class="note-body">${note.descHtml || ""}</div>`;
renderModalTags(tagsContainer, visibleTags);
if (editLink) {
editLink.href = note.editUrl || "#";
}
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(note);
modalColorPopup.classList.remove("open");
}
setModalPinButtonState(pinButton, !!note.isPinned);
content.innerHTML = html;
modal.classList.add("open");
}
function generatePaletteButtons(note) {
const colors = ["default", "red", "orange", "yellow", "green", "teal", "blue", "darkblue", "purple", "pink", "brown", "grey"];
// Map to hex for the button background
const colorMap = {
default: "#ffffff",
red: "#f28b82",
orange: "#fbbc04",
yellow: "#fff475",
green: "#ccff90",
teal: "#a7ffeb",
blue: "#cbf0f8",
darkblue: "#aecbfa",
purple: "#d7aefb",
pink: "#fdcfe8",
brown: "#e6c9a8",
grey: "#e8eaed",
};
// Dark mode mapping could be handled via CSS classes on buttons but inline styles are easier for the picker circles
// We will just use class names and let CSS handle the preview color if possible, or set style.
function generateModalPaletteButtons(note) {
const currentColor = note.color || "default";
const currentBackground = note.background || "none";
return colors
.map((c) => {
// We use style for the button background roughly.
// Actually, let's use the class on the button itself to pick up the color from CSS variables if defined,
// OR just hardcode the light mode preview for simplicity as the picker is usually on white/dark background.
return `<button class="palette-btn note-color-${c}" title="${c}" onclick="setNoteColor('${note.id}', '${c}', '${note.editUrl}')" style="background-color:${colorMap[c]}"></button>`;
})
.join("");
const colorButtons = [
`<button class="palette-btn palette-btn-default ${currentColor === "default" ? "is-active" : ""}" title="Par défaut" onclick="setModalNoteColor('default')"><i class="mdi mdi-format-color-reset"></i></button>`,
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
(opt) =>
`<button class="palette-btn note-color-${opt.key} ${currentColor === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setModalNoteColor('${opt.key}')" style="background-color:${opt.hex}"></button>`,
),
].join("");
const backgroundButtons = [
`<button class="palette-btn palette-btn-bg-none ${currentBackground === "none" ? "is-active" : ""}" title="Sans image" onclick="setModalNoteBackground('none')"><i class="mdi mdi-image-off-outline"></i></button>`,
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
const bgUrl = getNoteBackgroundUrl(bg.key);
return `<button class="palette-btn palette-btn-bg ${currentBackground === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setModalNoteBackground('${bg.key}')" style="background-image:url('${bgUrl}')"></button>`;
}),
].join("");
return `
<div class="palette-row palette-row-colors">${colorButtons}</div>
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
`;
}
window.setModalNoteColor = function (color) {
const modal = document.querySelector(".note-modal-overlay");
if (!modal || !modal.currentNote) return;
const currentNote = modal.currentNote;
setNoteColor(currentNote.id, color, currentNote.editUrl);
currentNote.color = color;
const modalCard = modal.querySelector(".note-modal");
if (modalCard) {
applyNoteVisualState(modalCard, currentNote);
}
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote);
modalColorPopup.classList.add("open");
positionPalettePopup(modalColorPopup);
}
};
window.setModalNoteBackground = function (backgroundKey) {
const modal = document.querySelector(".note-modal-overlay");
if (!modal || !modal.currentNote) return;
const currentNote = modal.currentNote;
setNoteBackground(currentNote.id, backgroundKey, currentNote.editUrl);
currentNote.background = backgroundKey;
const modalCard = modal.querySelector(".note-modal");
if (modalCard) {
applyNoteVisualState(modalCard, currentNote);
}
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(currentNote);
modalColorPopup.classList.add("open");
positionPalettePopup(modalColorPopup);
}
};
function generatePaletteButtons(note) {
const currentColor = note.color || "default";
const currentBackground = note.background || "none";
const colorButtons = [
`<button class="palette-btn palette-btn-default ${currentColor === "default" ? "is-active" : ""}" title="Par défaut" onclick="setNoteColor('${note.id}', 'default', '${note.editUrl}')"><i class="mdi mdi-format-color-reset"></i></button>`,
...NOTE_COLOR_OPTIONS.filter((opt) => opt.key !== "default").map(
(opt) =>
`<button class="palette-btn note-color-${opt.key} ${currentColor === opt.key ? "is-active" : ""}" title="${opt.label}" onclick="setNoteColor('${note.id}', '${opt.key}', '${note.editUrl}')" style="background-color:${opt.hex}"></button>`,
),
].join("");
const backgroundButtons = [
`<button class="palette-btn palette-btn-bg-none ${currentBackground === "none" ? "is-active" : ""}" title="Sans image" onclick="setNoteBackground('${note.id}', 'none', '${note.editUrl}')"><i class="mdi mdi-image-off-outline"></i></button>`,
...NOTE_BACKGROUND_OPTIONS.map((bg) => {
const bgUrl = getNoteBackgroundUrl(bg.key);
return `<button class="palette-btn palette-btn-bg ${currentBackground === bg.key ? "is-active" : ""}" title="${bg.label}" onclick="setNoteBackground('${note.id}', '${bg.key}', '${note.editUrl}')" style="background-image:url('${bgUrl}')"></button>`;
}),
].join("");
return `
<div class="palette-row palette-row-colors">${colorButtons}</div>
<div class="palette-row palette-row-backgrounds">${backgroundButtons}</div>
`;
}
window.setNoteColor = function (noteId, color, editUrl) {
// 1. Visual Update (Immediate feedback)
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
if (card) {
// Remove all color classes
card.classList.forEach((cls) => {
if (cls.startsWith("note-color-")) card.classList.remove(cls);
});
card.classList.add(`note-color-${color}`);
const background = card.dataset.background || "none";
applyNoteVisualState(card, { color, background });
}
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
if (bookmarkCard) {
const background = bookmarkCard.dataset.background || "none";
applyNoteVisualState(bookmarkCard, { color, background });
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
if (palettePopup) {
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, background);
}
}
const modal = document.querySelector(".note-modal-overlay");
if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) {
modal.currentNote.color = color;
const modalCard = modal.querySelector(".note-modal");
if (modalCard) {
applyNoteVisualState(modalCard, modal.currentNote);
}
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote);
}
}
// 2. Persistence via AJAX Form Submission
@ -592,7 +1120,11 @@ window.setNoteColor = function (noteId, color, editUrl) {
let tagsArray = currentTags.split(/[\s,]+/).filter((t) => t.trim() !== "");
// Remove existing color tags
tagsArray = tagsArray.filter((t) => !t.startsWith("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 new color tag (unless default)
if (color !== "default") {
@ -624,6 +1156,130 @@ window.setNoteColor = function (noteId, color, editUrl) {
});
};
window.setNoteBackground = function (noteId, backgroundKey, editUrl) {
const card = document.querySelector(`.note-card[data-id="${noteId}"]`);
if (card) {
const colorClass = Array.from(card.classList).find((cls) => cls.startsWith("note-color-"));
const color = colorClass ? colorClass.replace("note-color-", "") : card.dataset.color || "default";
applyNoteVisualState(card, { color, background: backgroundKey });
}
const bookmarkCard = document.querySelector(`.link-outer[data-id="${noteId}"]`);
if (bookmarkCard) {
const colorClass = Array.from(bookmarkCard.classList).find((cls) => cls.startsWith("note-color-"));
const color = colorClass ? colorClass.replace("note-color-", "") : bookmarkCard.dataset.color || "default";
applyNoteVisualState(bookmarkCard, { color, background: backgroundKey });
const palettePopup = bookmarkCard.querySelector(".bookmark-palette .palette-popup");
if (palettePopup) {
palettePopup.innerHTML = generateBookmarkPaletteButtons(noteId, editUrl, color, backgroundKey);
}
}
const modal = document.querySelector(".note-modal-overlay");
if (modal && modal.currentNote && String(modal.currentNote.id) === String(noteId)) {
modal.currentNote.background = backgroundKey;
const modalCard = modal.querySelector(".note-modal");
if (modalCard) {
applyNoteVisualState(modalCard, modal.currentNote);
}
const modalColorPopup = modal.querySelector("#note-modal-color-popup");
if (modalColorPopup) {
modalColorPopup.innerHTML = generateModalPaletteButtons(modal.currentNote);
}
}
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() !== "");
tagsArray = tagsArray.filter((t) => !t.startsWith(NOTE_BACKGROUND_TAG_PREFIX));
if (backgroundKey && backgroundKey !== "none") {
tagsArray.push(`${NOTE_BACKGROUND_TAG_PREFIX}${backgroundKey}`);
}
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 background");
}
})
.catch((err) => {
console.error("Error saving note background:", err);
alert("Erreur lors de la sauvegarde du fond. Veuillez rafraîchir la page.");
});
};
function addTagToNote(editUrl, tag) {
return 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() !== "");
if (!tagsArray.includes(tag)) tagsArray.push(tag);
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 update tags");
return response;
});
}
/* ==========================================================
PINNED ITEMS LOGIC (Tag: shaarli-pin)
========================================================== */