diff --git a/README.md b/README.md index 5b96a65d8..be62b5370 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q Trilium Screenshot +## ⏬ Download +- [Latest release](https://github.com/TriliumNext/Trilium/releases/latest) – stable version, recommended for most users. +- [Nightly build](https://github.com/TriliumNext/Trilium/releases/tag/nightly) – unstable development version, updated daily with the latest features and fixes. + ## 📚 Documentation **Visit our comprehensive documentation at [docs.triliumnotes.org](https://docs.triliumnotes.org/)** diff --git a/apps/client/package.json b/apps/client/package.json index 4459d2cb2..dceb6e8b8 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -35,6 +35,7 @@ "autocomplete.js": "0.38.1", "bootstrap": "5.3.8", "boxicons": "2.1.4", + "color": "5.0.2", "dayjs": "1.11.18", "dayjs-plugin-utc": "0.1.2", "debounce": "2.2.0", diff --git a/apps/client/src/services/css_class_manager.ts b/apps/client/src/services/css_class_manager.ts index f5d2e9649..fc839c9b1 100644 --- a/apps/client/src/services/css_class_manager.ts +++ b/apps/client/src/services/css_class_manager.ts @@ -1,21 +1,40 @@ +import {readCssVar} from "../utils/css-var"; +import Color, { ColorInstance } from "color"; + const registeredClasses = new Set(); -function createClassForColor(color: string | null) { - if (!color?.trim()) { - return ""; - } +// Read the color lightness limits defined in the theme as CSS variables - const normalizedColorName = color.replace(/[^a-z0-9]/gi, ""); +const lightThemeColorMaxLightness = readCssVar( + document.documentElement, + "tree-item-light-theme-max-color-lightness" + ).asNumber(70); - if (!normalizedColorName.trim()) { - return ""; - } +const darkThemeColorMinLightness = readCssVar( + document.documentElement, + "tree-item-dark-theme-min-color-lightness" + ).asNumber(50); - const className = `color-${normalizedColorName}`; +function createClassForColor(colorString: string | null) { + if (!colorString?.trim()) return ""; + + const color = parseColor(colorString); + if (!color) return ""; + + const className = `color-${color.hex().substring(1)}`; if (!registeredClasses.has(className)) { - // make the active fancytree selector more specific than the normal color setting - $("head").append(``); + const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!, + darkThemeColorMinLightness!); + + $("head").append(``); registeredClasses.add(className); } @@ -23,6 +42,44 @@ function createClassForColor(color: string | null) { return className; } +function parseColor(color: string) { + try { + return Color(color); + } catch (ex) { + console.error(ex); + } +} + +/** + * 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: ColorInstance, lightThemeMaxLightness: number, darkThemeMinLightness: number) { + const labColor = color.lab(); + const lightness = labColor.l(); + + // For the light theme, limit the maximum lightness + const lightThemeColor = labColor.l(Math.min(lightness, lightThemeMaxLightness)).hex(); + + // For the dark theme, limit the minimum lightness + const darkThemeColor = labColor.l(Math.max(lightness, darkThemeMinLightness)).hex(); + + let darkThemeBackgroundColor = "unset"; + let lightThemeBackgroundColor = "unset"; + + const hslColor = color.hsl(); + const hue = hslColor.hue(); + + if (color.saturationl() > 0) { + darkThemeBackgroundColor = Color({h: hue, s: 20, l: 33, alpha: .4}).hexa(); + lightThemeBackgroundColor = Color({h: hue, s: 37, l: 89, alpha: 1}).hexa(); + } + + return {lightThemeColor, lightThemeBackgroundColor, darkThemeColor, darkThemeBackgroundColor}; +} + export default { createClassForColor }; diff --git a/apps/client/src/stylesheets/theme-dark.css b/apps/client/src/stylesheets/theme-dark.css index f56e73232..95e6dccd8 100644 --- a/apps/client/src/stylesheets/theme-dark.css +++ b/apps/client/src/stylesheets/theme-dark.css @@ -82,6 +82,11 @@ body ::-webkit-calendar-picker-indicator { filter: invert(1); } +#left-pane span.fancytree-node { + --custom-color: var(--dark-theme-custom-color); + --custom-bg-color: var(--dark-theme-custom-bg-color); +} + .excalidraw.theme--dark { --theme-filter: invert(80%) hue-rotate(180deg) !important; } diff --git a/apps/client/src/stylesheets/theme-light.css b/apps/client/src/stylesheets/theme-light.css index 7aca1b3f9..933f5bfe6 100644 --- a/apps/client/src/stylesheets/theme-light.css +++ b/apps/client/src/stylesheets/theme-light.css @@ -81,3 +81,8 @@ html { --mermaid-theme: default; --native-titlebar-background: #ffffff00; } + +#left-pane span.fancytree-node { + --custom-color: var(--light-theme-custom-color); + --custom-bg-color: var(--light-theme-custom-bg-color); +} \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index c54cb26b7..b9a66da0d 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -268,6 +268,11 @@ * Dark color scheme tweaks */ +#left-pane span.fancytree-node { + --custom-color: var(--dark-theme-custom-color); + --custom-bg-color: var(--dark-theme-custom-bg-color); +} + body ::-webkit-calendar-picker-indicator { filter: invert(1); } @@ -278,4 +283,4 @@ body ::-webkit-calendar-picker-indicator { body .todo-list input[type="checkbox"]:not(:checked):before { border-color: var(--muted-text-color) !important; -} +} \ No newline at end of file diff --git a/apps/client/src/stylesheets/theme-next/base.css b/apps/client/src/stylesheets/theme-next/base.css index 1ec859c3a..a33da2bc5 100644 --- a/apps/client/src/stylesheets/theme-next/base.css +++ b/apps/client/src/stylesheets/theme-next/base.css @@ -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 perceptual lightness for the custom color in the light theme (%): */ + --tree-item-light-theme-max-color-lightness: 60; + + /* The minimum perceptual lightness for the custom color in the dark theme (%): */ + --tree-item-dark-theme-min-color-lightness: 65; } body.backdrop-effects-disabled { diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 5e713c249..b6bd38789 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -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 { @@ -658,7 +658,7 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu { inset-inline-start: var(--left-pane-item-selected-shadow-size); bottom: var(--left-pane-item-selected-shadow-size); inset-inline-end: var(--left-pane-item-selected-shadow-size); - background: var(--left-pane-item-selected-background) !important; + background: var(--custom-bg-color, var(--left-pane-item-selected-background)) !important; box-shadow: var(--left-pane-item-selected-shadow); border-radius: 6px; animation: left-pane-item-select 200ms ease-out; diff --git a/apps/client/src/stylesheets/tree.css b/apps/client/src/stylesheets/tree.css index a85ee7f63..9a718310e 100644 --- a/apps/client/src/stylesheets/tree.css +++ b/apps/client/src/stylesheets/tree.css @@ -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 { diff --git a/apps/client/src/utils/css-var.ts b/apps/client/src/utils/css-var.ts new file mode 100644 index 000000000..886247881 --- /dev/null +++ b/apps/client/src/utils/css-var.ts @@ -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 = parseFloat(this.value); + } + + return (!isNaN(number.valueOf()) ? number.valueOf() : defaultValue) + } + + asEnum(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)); + } + +} \ No newline at end of file diff --git a/docs/User Guide/User Guide/Advanced Usage/Attributes/Labels.md b/docs/User Guide/User Guide/Advanced Usage/Attributes/Labels.md index 53307238f..071bbb7cf 100644 --- a/docs/User Guide/User Guide/Advanced Usage/Attributes/Labels.md +++ b/docs/User Guide/User Guide/Advanced Usage/Attributes/Labels.md @@ -39,4 +39,4 @@ This is a list of labels that Trilium natively supports. > [!TIP] > Some labels presented here end with a `*`. That means that there are multiple labels with the same prefix, consult the specific page linked in the description of that label for more information. -
LabelDescription
disableVersioningDisables automatic creation of Note Revisions for a particular note. Useful for e.g. large, but unimportant notes - e.g. large JS libraries used for scripting.
versioningLimitLimits the maximum number of Note Revisions for a particular note, overriding the global settings.
calendarRootMarks the note which should be used as root for Day Notes. Only one should be marked as such.
archivedHides notes from default search results and dialogs. Archived notes can optionally be hidden in the Note Tree.
excludeFromExportExcludes this note and its children when exporting.
run, runOnInstance, runAtHourSee Events.
disableInclusionScripts with this label won't be included into parent script execution.
sorted

Keeps child notes sorted by title alphabetically.

When given a value, it will sort by the value of another label instead. If one of the child notes doesn't have the specified label, the title will be used for them instead.

sortDirection

If sorted is applied, specifies the direction of the sort:

  • ASC, ascending (default)
  • DESC, descending
sortFoldersFirstIf sorted is applied, folders (notes with children) will be sorted as a group at the top, and the rest will be sorted.
topIf sorted is applied to the parent note, keeps given note on top in its parent.
hidePromotedAttributesHide Promoted Attributes on this note. Generally useful when defining inherited attributes, but the parent note doesn't need them.
readOnlyMarks a note to be always be read-only, if it's a supported note (text, code, mermaid).
autoReadOnlyDisabledDisables automatic read-only mode for the given note.
appCssMarks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks. See Custom app-wide CSS for more info.
appThemeMarks CSS notes which are full Trilium themes and are thus available in Trilium options. See Theme development for more information.
appThemeBaseSet to next, next-light, or next-dark to use the corresponding TriliumNext theme (auto, light or dark) as the base for a custom theme, instead of the legacy one. See Customize the Next theme for more information.
cssClassValue of this label is then added as CSS class to the node representing given note in the Note Tree. This can be useful for advanced theming. Can be used in template notes.
iconClassvalue of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.
pageSizeSpecifies the number of items per page in Note List.
customRequestHandlerSee Custom Request Handler.
customResourceProviderSee Custom Resource Providers.
widgetMarks this note as a custom widget which will be added to the Trilium component tree. See Custom Widgets for more information.
searchHomeNew search notes will be created as children of this note (see Saved Search).
workspace and related attributesSee Workspaces.
inboxdefault inbox location for new notes - when you create a note using new note button in the sidebar, notes will be created as child notes in the note marked as with #inbox label.
sqlConsoleHomeDefault location of SQL Console notes
bookmarkedIndicates this note is a bookmark.
bookmarkFolderNote with this label will appear in bookmarks as folder (allowing access to its children). See Bookmarks for more information.
share*See the attribute reference in Sharing.
displayRelations, hideRelationsComma delimited names of relations which should be displayed/hidden in a Relation Map (both the note type and the Note Map (Link map, Tree map) general functionality).
titleTemplate

Default title of notes created as children of this note. This value is evaluated as a JavaScript string and thus can be enriched with dynamic content via the injected now and parentNote variables.

Examples:

  • \({parentNote.getLabel('authorName')}'s literary works
  • Log for \){now.format('YYYY-MM-DD HH:mm:ss')}
  • to mirror the parent's template.

See Default Note Title for more info.

templateThis note will appear in the selection of available template when creating new note. See Templates for more information.
tocControls the display of the Table of contents for a given note. #toc or #toc=show to always display the table of contents, #toc=false to always hide it.
colordefines color of the note in note tree, links etc. Use any valid CSS color value like 'red' or #a13d5f
keyboardShortcutDefines a keyboard shortcut which will immediately jump to this note. Example: 'ctrl+alt+e'. Requires frontend reload for the change to take effect.
keepCurrentHoistingOpening this link won't change hoisting even if the note is not displayable in the current hoisted subtree.
executeButtonTitle of the button which will execute the current code note
executeDescriptionLonger description of the current code note displayed together with the execute button
excludeFromNoteMapNotes with this label will be hidden from the Note Map.
newNotesOnTopNew notes will be created at the top of the parent note, not on the bottom.
hideHighlightWidgetHides the Highlights list widget
hideChildrenOverviewHides the Note List for that particular note.
printLandscapeWhen exporting to PDF, changes the orientation of the page to landscape instead of portrait.
printPageSizeWhen exporting to PDF, changes the size of the page. Supported values: A0, A1, A2, A3, A4, A5, A6, Legal, Letter, Tabloid, Ledger.
geolocationIndicates the latitude and longitude of a note, to be displayed in a Geo Map.
calendar:*Defines specific options for the Calendar View.
viewTypeSets the view of child notes (e.g. grid or list). See Note List for more information.
\ No newline at end of file +
LabelDescription
disableVersioningDisables automatic creation of Note Revisions for a particular note. Useful for e.g. large, but unimportant notes - e.g. large JS libraries used for scripting.
versioningLimitLimits the maximum number of Note Revisions for a particular note, overriding the global settings.
calendarRootMarks the note which should be used as root for Day Notes. Only one should be marked as such.
archivedHides notes from default search results and dialogs. Archived notes can optionally be hidden in the Note Tree.
excludeFromExportExcludes this note and its children when exporting.
run, runOnInstance, runAtHourSee Events.
disableInclusionScripts with this label won't be included into parent script execution.
sorted

Keeps child notes sorted by title alphabetically.

When given a value, it will sort by the value of another label instead. If one of the child notes doesn't have the specified label, the title will be used for them instead.

sortDirection

If sorted is applied, specifies the direction of the sort:

  • ASC, ascending (default)
  • DESC, descending
sortFoldersFirstIf sorted is applied, folders (notes with children) will be sorted as a group at the top, and the rest will be sorted.
topIf sorted is applied to the parent note, keeps given note on top in its parent.
hidePromotedAttributesHide Promoted Attributes on this note. Generally useful when defining inherited attributes, but the parent note doesn't need them.
readOnlyMarks a note to be always be read-only, if it's a supported note (text, code, mermaid).
autoReadOnlyDisabledDisables automatic read-only mode for the given note.
appCssMarks CSS notes which are loaded into the Trilium application and can thus be used to modify Trilium's looks. See Custom app-wide CSS for more info.
appThemeMarks CSS notes which are full Trilium themes and are thus available in Trilium options. See Theme development for more information.
appThemeBaseSet to next, next-light, or next-dark to use the corresponding TriliumNext theme (auto, light or dark) as the base for a custom theme, instead of the legacy one. See Customize the Next theme for more information.
cssClassValue of this label is then added as CSS class to the node representing given note in the Note Tree. This can be useful for advanced theming. Can be used in template notes.
iconClassvalue of this label is added as a CSS class to the icon on the tree which can help visually distinguish the notes in the tree. Example might be bx bx-home - icons are taken from boxicons. Can be used in template notes.
pageSizeSpecifies the number of items per page in Note List.
customRequestHandlerSee Custom Request Handler.
customResourceProviderSee Custom Resource Providers.
widgetMarks this note as a custom widget which will be added to the Trilium component tree. See Custom Widgets for more information.
searchHomeNew search notes will be created as children of this note (see Saved Search).
workspace and related attributesSee Workspaces.
inboxdefault inbox location for new notes - when you create a note using new note button in the sidebar, notes will be created as child notes in the note marked as with #inbox label.
sqlConsoleHomeDefault location of SQL Console notes
bookmarkedIndicates this note is a bookmark.
bookmarkFolderNote with this label will appear in bookmarks as folder (allowing access to its children). See Bookmarks for more information.
share*See the attribute reference in Sharing.
displayRelations, hideRelationsComma delimited names of relations which should be displayed/hidden in a Relation Map (both the note type and the Note Map (Link map, Tree map) general functionality).
titleTemplate

Default title of notes created as children of this note. This value is evaluated as a JavaScript string and thus can be enriched with dynamic content via the injected now and parentNote variables.

Examples:

  • \({parentNote.getLabel('authorName')}'s literary works
  • Log for \){now.format('YYYY-MM-DD HH:mm:ss')}
  • to mirror the parent's template.

See Default Note Title for more info.

templateThis note will appear in the selection of available template when creating new note. See Templates for more information.
tocControls the display of the Table of contents for a given note. #toc or #toc=show to always display the table of contents, #toc=false to always hide it.
colordefines color of the note in note tree, links etc. Use any valid CSS color value like 'red' or #a13d5f
Note: this color may be automatically adjusted when displayed to ensure sufficient contrast with the background.
keyboardShortcutDefines a keyboard shortcut which will immediately jump to this note. Example: 'ctrl+alt+e'. Requires frontend reload for the change to take effect.
keepCurrentHoistingOpening this link won't change hoisting even if the note is not displayable in the current hoisted subtree.
executeButtonTitle of the button which will execute the current code note
executeDescriptionLonger description of the current code note displayed together with the execute button
excludeFromNoteMapNotes with this label will be hidden from the Note Map.
newNotesOnTopNew notes will be created at the top of the parent note, not on the bottom.
hideHighlightWidgetHides the Highlights list widget
hideChildrenOverviewHides the Note List for that particular note.
printLandscapeWhen exporting to PDF, changes the orientation of the page to landscape instead of portrait.
printPageSizeWhen exporting to PDF, changes the size of the page. Supported values: A0, A1, A2, A3, A4, A5, A6, Legal, Letter, Tabloid, Ledger.
geolocationIndicates the latitude and longitude of a note, to be displayed in a Geo Map.
calendar:*Defines specific options for the Calendar View.
viewTypeSets the view of child notes (e.g. grid or list). See Note List for more information.
\ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bae171169..77e4d877e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -184,6 +184,9 @@ importers: boxicons: specifier: 2.1.4 version: 2.1.4 + color: + specifier: 5.0.2 + version: 5.0.2 dayjs: specifier: 1.11.18 version: 1.11.18 @@ -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: @@ -15083,6 +15092,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 +15263,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: @@ -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: {}