client/note tree: adjust the custom color of tree items to maintain readability

This commit is contained in:
Adorian Doran
2025-10-18 21:05:34 +03:00
parent 0d94ae9f61
commit 2b460be63a
10 changed files with 151 additions and 13 deletions

View File

@@ -1,5 +1,20 @@
import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color";
const registeredClasses = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables
const lightThemeColorMaxLightness = readCssVar(
document.documentElement,
"tree-item-light-theme-max-color-lightness"
).asNumber(70);
const darkThemeColorMinLightness = readCssVar(
document.documentElement,
"tree-item-dark-theme-min-color-lightness"
).asNumber(50);
function createClassForColor(color: string | null) {
if (!color?.trim()) {
return "";
@@ -13,9 +28,16 @@ function createClassForColor(color: string | null) {
const className = `color-${normalizedColorName}`;
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!, darkThemeColorMinLightness!);
if (!adjustedColor) return "";
if (!registeredClasses.has(className)) {
// make the active fancytree selector more specific than the normal color setting
$("head").append(`<style>.${className}, span.fancytree-active.${className} { color: ${color} !important; }</style>`);
$("head").append(`<style>
.${className}, span.fancytree-active.${className} {
--light-theme-custom-color: ${adjustedColor.lightThemeColor};
--dark-theme-custom-color: ${adjustedColor.darkThemeColor}
}
</style>`);
registeredClasses.add(className);
}
@@ -23,6 +45,31 @@ function createClassForColor(color: string | null) {
return className;
}
/**
* Returns a pair of colors — one optimized for light themes and the other for dark themes, derived
* from the specified color to maintain sufficient contrast with each theme.
* The adjustment is performed by limiting the colors lightness in the CIELAB color space,
* according to the lightThemeMaxLightness and darkThemeMinLightness parameters.
*/
function adjustColorLightness(color: string, lightThemeMaxLightness: number, darkThemeMinLightness: number) {
let labColor: ColorInstance | undefined = undefined;
try {
// Parse the given color in the CIELAB color space
labColor = Color(color).lab();
} catch (ex) {
console.error(`Failed to parse color: "${color}"`, ex);
return;
}
// For the light theme, limit the maximum lightness
const lightThemeColor = labColor.l(Math.min(labColor.l(), lightThemeMaxLightness)).hex();
// For the light theme, limit the minimum lightness
const darkThemeColor = labColor.l(Math.max(labColor.l(), darkThemeMinLightness)).hex();
return {lightThemeColor, darkThemeColor};
}
export default {
createClassForColor
};
};

View File

@@ -82,6 +82,10 @@ body ::-webkit-calendar-picker-indicator {
filter: invert(1);
}
#left-pane span.fancytree-node {
--custom-color: var(--dark-theme-custom-color);
}
.excalidraw.theme--dark {
--theme-filter: invert(80%) hue-rotate(180deg) !important;
}

View File

@@ -81,3 +81,7 @@ html {
--mermaid-theme: default;
--native-titlebar-background: #ffffff00;
}
#left-pane span.fancytree-node {
--custom-color: var(--light-theme-custom-color);
}

View File

@@ -268,6 +268,10 @@
* Dark color scheme tweaks
*/
#left-pane span.fancytree-node {
--custom-color: var(--dark-theme-custom-color);
}
body ::-webkit-calendar-picker-indicator {
filter: invert(1);
}
@@ -278,4 +282,4 @@ body ::-webkit-calendar-picker-indicator {
body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important;
}
}

View File

@@ -82,6 +82,20 @@
/* Theme capabilities */
--tab-note-icons: true;
/* To ensure that a tree item's custom color remains sufficiently contrasted and readable,
* the color is adjusted based on the current color scheme (light or dark). The lightness
* component of the color represented in the CIELAB color space, will be
* constrained to a certain percentage defined below.
*
* Note: the tree background may vary when background effects are enabled, so it is recommended
* to maintain a higher contrast margin than on the usual note tree solid background. */
/* The maximum lightness for the custom color in the light theme (%): */
--tree-item-light-theme-max-color-lightness: 50;
/* The minimum lightness for the custom color in the dark theme (%): */
--tree-item-dark-theme-min-color-lightness: 60;
}
body.backdrop-effects-disabled {

View File

@@ -639,7 +639,7 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu {
#left-pane span.fancytree-node.fancytree-active {
position: relative;
background: transparent !important;
color: var(--left-pane-item-selected-color);
color: var(--custom-color, var(--left-pane-item-selected-color));
}
@keyframes left-pane-item-select {

View File

@@ -40,6 +40,7 @@ span.fancytree-node.fancytree-hide {
text-overflow: ellipsis;
user-select: none !important;
-webkit-user-select: none !important;
color: var(--custom-color, inherit);
}
.fancytree-node:not(.fancytree-loading) .fancytree-expander {

View File

@@ -0,0 +1,45 @@
export function readCssVar(element: HTMLElement, varName: string) {
return new CssVarReader(getComputedStyle(element).getPropertyValue("--" + varName));
}
export class CssVarReader {
protected value: string;
constructor(rawValue: string) {
this.value = rawValue;
}
asString(defaultValue?: string) {
return (this.value) ? this.value : defaultValue;
}
asNumber(defaultValue?: number) {
let number: Number = NaN;
if (this.value) {
number = new Number(this.value);
}
return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue)
}
asEnum<T>(enumType: T, defaultValue?: T[keyof T]): T[keyof T] | undefined {
let result: T[keyof T] | undefined;
result = enumType[this.value as keyof T];
if (result === undefined) {
result = defaultValue;
}
return result;
}
asArray(delimiter: string = " "): CssVarReader[] {
// Note: ignoring delimiters inside quotation marks is currently unsupported
let values = this.value.split(delimiter);
return values.map((v) => new CssVarReader(v));
}
}

View File

@@ -26,6 +26,7 @@
},
"dependencies": {
"better-sqlite3": "12.4.1",
"color": "5.0.2",
"node-html-parser": "7.0.1"
},
"devDependencies": {

34
pnpm-lock.yaml generated
View File

@@ -452,6 +452,9 @@ importers:
better-sqlite3:
specifier: 12.4.1
version: 12.4.1
color:
specifier: 5.0.2
version: 5.0.2
node-html-parser:
specifier: 7.0.1
version: 7.0.1
@@ -6328,10 +6331,18 @@ packages:
color-parse@2.0.2:
resolution: {integrity: sha512-eCtOz5w5ttWIUcaKLiktF+DxZO1R9KLNY/xhbV6CkhM7sR3GhVghmt6X6yOnzeaM24po+Z9/S1apbXMwA3Iepw==}
color-string@2.1.2:
resolution: {integrity: sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==}
engines: {node: '>=18'}
color-support@1.1.3:
resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==}
hasBin: true
color@5.0.2:
resolution: {integrity: sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==}
engines: {node: '>=18'}
colord@2.9.3:
resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==}
@@ -14681,8 +14692,6 @@ snapshots:
'@ckeditor/ckeditor5-core': 47.1.0
'@ckeditor/ckeditor5-upload': 47.1.0
ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-ai@47.1.0(bufferutil@4.0.9)(utf-8-validate@6.0.5)':
dependencies:
@@ -14892,6 +14901,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
'@ckeditor/ckeditor5-watchdog': 47.1.0
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-dev-build-tools@43.1.0(@swc/helpers@0.5.17)(tslib@2.8.1)(typescript@5.9.3)':
dependencies:
@@ -15083,6 +15094,8 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
es-toolkit: 1.39.5
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-editor-multi-root@47.1.0':
dependencies:
@@ -15252,8 +15265,6 @@ snapshots:
'@ckeditor/ckeditor5-utils': 47.1.0
'@ckeditor/ckeditor5-widget': 47.1.0
ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-html-embed@47.1.0':
dependencies:
@@ -15669,8 +15680,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.1.0
'@ckeditor/ckeditor5-utils': 47.1.0
ckeditor5: 47.1.0(patch_hash=8331a09d41443b39ea1c784daaccfeb0da4f9065ed556e7de92e9c77edd9eb41)
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-special-characters@47.1.0':
dependencies:
@@ -16338,7 +16347,7 @@ snapshots:
make-fetch-happen: 10.2.1
nopt: 6.0.0
proc-log: 2.0.1
semver: 7.7.2
semver: 7.7.3
tar: 6.2.1
which: 2.0.2
transitivePeerDependencies:
@@ -17685,7 +17694,7 @@ snapshots:
'@npmcli/fs@2.1.2':
dependencies:
'@gar/promisify': 1.1.3
semver: 7.7.2
semver: 7.7.3
'@npmcli/fs@4.0.0':
dependencies:
@@ -21148,9 +21157,18 @@ snapshots:
dependencies:
color-name: 2.0.0
color-string@2.1.2:
dependencies:
color-name: 2.0.0
color-support@1.1.3:
optional: true
color@5.0.2:
dependencies:
color-convert: 3.1.0
color-string: 2.1.2
colord@2.9.3: {}
colorette@2.0.20: {}