mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 00:26:23 +02:00
client/note tree: adjust the custom color of tree items to maintain readability
This commit is contained in:
@@ -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 color’s 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
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -81,3 +81,7 @@ html {
|
||||
--mermaid-theme: default;
|
||||
--native-titlebar-background: #ffffff00;
|
||||
}
|
||||
|
||||
#left-pane span.fancytree-node {
|
||||
--custom-color: var(--light-theme-custom-color);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
45
apps/client/src/utils/css-var.ts
Normal file
45
apps/client/src/utils/css-var.ts
Normal 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));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user