Merge remote-tracking branch 'origin/main' into renovate/express-serve-static-core-5.x
Some checks are pending
Checks / main (push) Waiting to run

This commit is contained in:
Elian Doran 2026-02-28 19:11:57 +02:00
commit 34ca7912fc
No known key found for this signature in database
52 changed files with 1122 additions and 289 deletions

View File

@ -14,7 +14,7 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.30.2",
"packageManager": "pnpm@10.30.3",
"devDependencies": {
"@redocly/cli": "2.19.2",
"archiver": "7.0.1",

View File

@ -22,6 +22,7 @@
"@fullcalendar/interaction": "6.1.20",
"@fullcalendar/list": "6.1.20",
"@fullcalendar/multimonth": "6.1.20",
"@fullcalendar/rrule": "6.1.20",
"@fullcalendar/timegrid": "6.1.20",
"@maplibre/maplibre-gl-leaflet": "0.1.3",
"@mermaid-js/layout-elk": "0.2.0",
@ -63,6 +64,7 @@
"react-i18next": "16.5.4",
"react-window": "2.2.7",
"reveal.js": "5.2.1",
"rrule": "2.8.1",
"svg-pan-zoom": "3.6.2",
"tabulator-tables": "6.3.1",
"vanilla-js-wheel-zoom": "9.0.4"

View File

@ -3,6 +3,8 @@ import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu, { type MenuItem } from "./context_menu.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import * as clipboardExt from "../services/clipboard_ext.js";
import type { BrowserWindow } from "electron";
import type { CommandNames, AppContext } from "../components/app_context.js";
@ -60,6 +62,33 @@ function setupContextMenu() {
uiIcon: "bx bx-copy",
handler: () => webContents.copy()
});
items.push({
enabled: hasText,
title: t("electron_context_menu.copy-as-markdown"),
uiIcon: "bx bx-copy-alt",
handler: async () => {
const selection = window.getSelection();
if (!selection || !selection.rangeCount) return '';
const range = selection.getRangeAt(0);
const div = document.createElement('div');
div.appendChild(range.cloneContents());
const htmlContent = div.innerHTML;
if (htmlContent) {
try {
const { markdownContent } = await server.post<{ markdownContent: string }>(
"other/to-markdown",
{ htmlContent }
);
await clipboardExt.copyTextWithToast(markdownContent);
} catch (error) {
console.error("Failed to copy as markdown:", error);
}
}
}
});
}
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === "none") {

View File

@ -16,6 +16,8 @@ import protectedSessionHolder from "./protected_session_holder.js";
import renderService from "./render.js";
import { applySingleBlockSyntaxHighlight } from "./syntax_highlight.js";
import utils, { getErrorMessage } from "./utils.js";
import PdfViewer from "../widgets/type_widgets/file/PdfViewer";
import { h, render } from "preact";
let idCounter = 1;
@ -195,10 +197,13 @@ function renderFile(entity: FNote | FAttachment, type: string, $renderedContent:
const $content = $('<div style="display: flex; flex-direction: column; height: 100%; justify-content: end;">');
if (type === "pdf") {
const $pdfPreview = $('<iframe class="pdf-preview" style="width: 100%; flex-grow: 100;"></iframe>');
$pdfPreview.attr("src", openService.getUrlForDownload(`pdfjs/web/viewer.html?file=../../api/${entityType}/${entityId}/open`));
const url = `../../api/${entityType}/${entityId}/open`;
const $viewer = $(`<div style="height: 100%">`);
render(h(PdfViewer, {pdfUrl: url, editable: false}), $viewer.get(0)!);
$content.append($viewer);
$content.append($pdfPreview);
} else if (type === "audio") {
const $audioPreview = $("<audio controls></audio>")
.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`))

View File

@ -647,10 +647,10 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
font-weight: 600;
}
.ck-content hr {
margin: 5px 0;
height: 1px;
background-color: var(--main-border-color);
:root .ck-content hr {
margin-block: 5px;
height: 0;
border: thin solid var(--main-border-color);
opacity: 1;
}

View File

@ -372,10 +372,6 @@ body[dir=ltr] #launcher-container {
.calendar-dropdown-widget .calendar-header [data-calendar-input="month"] {
--input-background-color: transparent;
--menu-background-color: transparent;
text-align: center;
font-size: 1.4em;
font-weight: 300;
}
.calendar-dropdown-widget .calendar-header input:not(:focus) {
@ -425,8 +421,6 @@ body[dir=ltr] #launcher-container {
}
.calendar-dropdown-widget .calendar-week span {
font-size: 0.85em;
font-weight: 500;
color: var(--calendar-weekday-labels-color);
}
@ -689,9 +683,10 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu {
padding-inline-start: 12px;
}
#left-pane span.fancytree-node.fancytree-active {
#left-pane span.fancytree-node.fancytree-active,
#left-pane span.fancytree-node.fancytree-active:hover {
position: relative;
background: transparent !important;
background: transparent;
color: var(--custom-color, var(--left-pane-item-selected-color));
}
@ -704,6 +699,14 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu {
}
}
/*
* .fancytree-node pseudo-elements:
*
* - ::before: the active tree item decorator.
* - ::after: the selected tree item background. A pseudo-element is used instead of the
* element's background color, to allow alpha compositing for the hover state.
*/
#left-pane span.fancytree-node.fancytree-active::before {
position: absolute;
content: "";
@ -718,6 +721,24 @@ body.layout-vertical.background-effects div.quick-search .dropdown-menu {
z-index: -1;
}
#left-pane span.fancytree-node.fancytree-selected {
--left-pane-item-selected-shadow-size: 4px;
position: relative;
background-color: transparent;
border-radius: 0;
}
#left-pane span.fancytree-node.fancytree-selected::after {
display: block;
position: absolute;
z-index: -2;
content: "";
inset: 0;
background: var(--selection-background-color);
animation: left-pane-item-select 100ms ease-out;
}
#left-pane span.fancytree-node.protected > span.fancytree-custom-icon {
position: relative;
filter: unset !important;
@ -780,7 +801,8 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
opacity: 0.5;
}
#left-pane .tree-item-button {
#left-pane .tree-item-button,
#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon {
margin-inline-end: 6px;
border: unset;
border-radius: 50%;
@ -791,7 +813,8 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
box-shadow 200ms ease-out;
}
#left-pane .tree-item-button:hover {
#left-pane .tree-item-button:hover,
#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon:hover {
background: var(--left-pane-item-action-button-hover-background);
box-shadow: var(--left-pane-item-action-button-hover-shadow);
transition:
@ -799,10 +822,41 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
box-shadow 100ms ease-in;
}
#left-pane span.fancytree-node.fancytree-active .tree-item-button:hover {
#left-pane span.fancytree-node.fancytree-active .tree-item-button:hover,
#left-pane span.fancytree-node.fancytree-active.fancytree-selected .fancytree-custom-icon:hover {
box-shadow: var(--left-pane-item-selected-action-button-hover-shadow);
}
/* Selected item bulk action button */
@keyframes bulk-action-button-blink {
from {
opacity: 1;
}
to {
opacity: .3;
}
}
#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon {
margin: 0;
}
#left-pane span.fancytree-node.fancytree-selected .fancytree-custom-icon::before {
border: 0;
font-size: .65em;
}
#left-pane span.fancytree-node.fancytree-selected:hover .fancytree-custom-icon:not(:hover)::before {
animation: bulk-action-button-blink 500ms linear infinite alternate;
}
#left-pane span.fancytree-node.fancytree-selected.protected .fancytree-custom-icon::after {
/* Protected note indicator */
display: none;
}
#context-menu-container {
/* The context menu of the tree */
--menu-item-icon-vert-offset: -1px;
@ -1033,7 +1087,7 @@ body.layout-vertical.electron.platform-darwin .tab-row-container {
height: var(--tab-height) !important;
}
.tab-row-widget > * {
body.layout-vertical .tab-row-widget > * {
margin-top: calc((var(--tab-bar-height) - var(--tab-height)) / 2);
}

View File

@ -140,10 +140,22 @@ ul.fancytree-container {
background-color: inherit;
}
.fancytree-custom-icon {
display: flex;
justify-content: center;
align-items: center;
width: 1em;
height: 1em;
font-size: 1.2em;
}
/* Fallback icon */
:where(.fancytree-custom-icon)::before {
content: "?";
}
/* Protected note icon badge */
span.fancytree-node.protected > span.fancytree-custom-icon {
filter: drop-shadow(2px 2px 2px var(--main-text-color));
}
@ -185,7 +197,7 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
span.fancytree-active {
color: var(--active-item-text-color);
background-color: var(--active-item-background-color) !important;
background-color: var(--active-item-background-color);
border-color: transparent; /* invisible border */
border-radius: 5px;
}
@ -195,20 +207,15 @@ span.fancytree-active .fancytree-title {
border: 0;
}
span.fancytree-selected {
border-color: var(--main-border-color) !important;
border-radius: 5px;
}
span.fancytree-selected .fancytree-title {
text-decoration: underline;
font-style: italic;
span.fancytree-node.fancytree-selected {
background-color: var(--selection-background-color);
border-radius: 0;
}
span.fancytree-selected .fancytree-custom-icon::before {
font-family: "boxicons";
content: "\eb43";
border: 1px solid var(--main-border-color);
content: "\ef05";
border: 1px solid var(--main-text-color);
border-radius: 3px;
}

View File

@ -882,6 +882,7 @@
"electron_context_menu": {
"cut": "قص",
"copy": "نسخ",
"copy-as-markdown": "نسخ كـ Markdown",
"paste": "لصق",
"copy-link": "نسخ الرابط",
"add-term-to-dictionary": "اضافة \"{{term}}\" الى القاموس",

View File

@ -1760,6 +1760,7 @@
"add-term-to-dictionary": "将 \"{{term}}\" 添加到字典",
"cut": "剪切",
"copy": "复制",
"copy-as-markdown": "复制为 Markdown",
"copy-link": "复制链接",
"paste": "粘贴",
"paste-as-plain-text": "以纯文本粘贴",

View File

@ -1729,6 +1729,7 @@
"add-term-to-dictionary": "Begriff \"{{term}}\" zum Wörterbuch hinzufügen",
"cut": "Ausschneiden",
"copy": "Kopieren",
"copy-as-markdown": "Als Markdown kopieren",
"copy-link": "Link kopieren",
"paste": "Einfügen",
"paste-as-plain-text": "Als unformatierten Text einfügen",

View File

@ -1810,6 +1810,7 @@
"add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
"cut": "Cut",
"copy": "Copy",
"copy-as-markdown": "Copy as Markdown",
"copy-link": "Copy link",
"paste": "Paste",
"paste-as-plain-text": "Paste as plain text",

View File

@ -1778,6 +1778,7 @@
"add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario",
"cut": "Cortar",
"copy": "Copiar",
"copy-as-markdown": "Copiar como markdown",
"copy-link": "Copiar enlace",
"paste": "Pegar",
"paste-as-plain-text": "Pegar como texto plano",

View File

@ -1689,6 +1689,7 @@
"add-term-to-dictionary": "Ajouter «{{term}}» au dictionnaire",
"cut": "Couper",
"copy": "Copier",
"copy-as-markdown": "Copier en markdown",
"copy-link": "Copier le lien",
"paste": "Coller",
"paste-as-plain-text": "Coller comme texte brut",

View File

@ -1808,6 +1808,7 @@
"add-term-to-dictionary": "Cuir \"{{term}}\" leis an bhfoclóir",
"cut": "Gearr",
"copy": "Cóipeáil",
"copy-as-markdown": "Cóipeáil mar markdown",
"copy-link": "Cóipeáil nasc",
"paste": "Greamaigh",
"paste-as-plain-text": "Greamaigh mar théacs simplí",

View File

@ -1810,6 +1810,7 @@
"add-term-to-dictionary": "\"{{term}}\" को डिक्शनरी में जोड़ें",
"cut": "कट (Cut)",
"copy": "कॉपी (Copy)",
"copy-as-markdown": "Markdown के रूप में कॉपी करें",
"copy-link": "लिंक कॉपी करें",
"paste": "पेस्ट (Paste)",
"paste-as-plain-text": "प्लेन टेक्स्ट की तरह पेस्ट करें",

View File

@ -334,6 +334,7 @@
"electron_context_menu": {
"cut": "Taglia",
"copy": "Copia",
"copy-as-markdown": "Copia come markdown",
"paste": "Incolla",
"copy-link": "Copia collegamento",
"paste-as-plain-text": "Incolla come testo semplice",
@ -519,7 +520,7 @@
"custom_name_label": "Nome del motore di ricerca personalizzato",
"custom_name_placeholder": "Personalizza il nome del motore di ricerca",
"custom_url_label": "L'URL del motore di ricerca personalizzato deve includere {keyword} come segnaposto per il termine di ricerca.",
"custom_url_placeholder": "Personalizza l'URL del motore di ricerca"
"custom_url_placeholder": "Personalizza l'URL del motore di ricerca"
},
"sql_table_schemas": {
"tables": "Tabelle"
@ -592,7 +593,7 @@
"collapseExpand": "collassa/espande il nodo",
"notSet": "non impostato",
"goBackForwards": "indietro/avanti nella cronologia",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Vai a\"</a>",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">finestra \"Vai a\"</a>",
"title": "Scheda riassuntiva",
"noteNavigation": "Nota navigazione",
"scrollToActiveNote": "scorri fino alla nota attiva",

View File

@ -1353,6 +1353,7 @@
"add-term-to-dictionary": "辞書に \"{{term}}\" を追加",
"cut": "切り取り",
"copy": "コピー",
"copy-as-markdown": "Markdownとしてコピー",
"copy-link": "リンクをコピー",
"paste": "貼り付け",
"paste-as-plain-text": "プレーンテキストで貼り付け",

View File

@ -261,7 +261,9 @@
"percentage": "%"
},
"pagination": {
"total_notes": "{{count}} notatek"
"total_notes": "{{count}} notatek",
"prev_page": "Poprzednia strona",
"next_page": "Następna strona"
},
"collections": {
"rendering_error": "Nie można wyświetlić zawartości z powodu błędu."
@ -612,6 +614,7 @@
"electron_context_menu": {
"cut": "Wytnij",
"copy": "Kopiuj",
"copy-as-markdown": "Kopiuj jako markdown",
"copy-link": "Kopiuj link",
"paste": "Wklej",
"paste-as-plain-text": "Wklej jako zwykły tekst",
@ -737,7 +740,8 @@
"raster": "Raster",
"vector_light": "Wektor (Jasny)",
"vector_dark": "Wektor (Ciemny)",
"show-scale": "Pokaż skalę"
"show-scale": "Pokaż skalę",
"show-labels": "Pokaż nazwy znaczników"
},
"table_context_menu": {
"delete_row": "Usuń wiersz"
@ -1232,7 +1236,7 @@
"no_attachments": "Ta notatka nie ma załączników."
},
"book": {
"no_children_help": "Ta kolekcja nie posiada żadnych notatek podrzędnych, więc nie ma nic do wyświetlenia. Zobacz <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> po szczegóły.",
"no_children_help": "Ta kolekcja nie zawiera notatek podrzędnych, więc nie ma nic do wyświetlenia.",
"drag_locked_title": "Zablokowane do edycji",
"drag_locked_message": "Przeciąganie niedozwolone, ponieważ kolekcja jest zablokowana do edycji."
},
@ -1653,7 +1657,8 @@
"description": "Opis",
"reload_app": "Przeładuj aplikację, aby zastosować zmiany",
"set_all_to_default": "Ustaw wszystkie skróty na domyślne",
"confirm_reset": "Czy na pewno chcesz zresetować wszystkie skróty klawiszowe do domyślnych?"
"confirm_reset": "Czy na pewno chcesz zresetować wszystkie skróty klawiszowe do domyślnych?",
"no_results": "Nie znaleziono skrótów pasujących do '{{filter}}'"
},
"spellcheck": {
"title": "Sprawdzanie pisowni",
@ -1860,7 +1865,9 @@
"print_report_collection_content_few": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
"print_report_collection_content_many": "Nie można wydrukować {{count}} notatek w kolekcji, ponieważ nie są one obsługiwane lub są chronione.",
"print_report_collection_details_button": "Zobacz szczegóły",
"print_report_collection_details_ignored_notes": "Zignorowane notatki"
"print_report_collection_details_ignored_notes": "Zignorowane notatki",
"print_report_error_title": "Nie udało się wydrukować",
"print_report_stack_trace": "Ślad stosu"
},
"note_title": {
"placeholder": "wpisz tytuł notatki tutaj...",
@ -1875,7 +1882,8 @@
},
"search_result": {
"no_notes_found": "Nie znaleziono notatek dla podanych parametrów wyszukiwania.",
"search_not_executed": "Wyszukiwanie nie zostało jeszcze wykonane. Kliknij przycisk \"Szukaj\" powyżej, aby zobaczyć wyniki."
"search_not_executed": "Nie przeprowadzono jeszcze wyszukiwania.",
"search_now": "Szukaj teraz"
},
"spacer": {
"configure_launchbar": "Konfiguruj pasek szybkiego dostępu"
@ -2145,5 +2153,49 @@
},
"bookmark_buttons": {
"bookmarks": "Zakładki"
},
"render": {
"setup_title": "Wyświetl własny kod HTML lub Preact JSX w tej notatce",
"setup_create_sample_preact": "Stwórz przykładową notatkę z użyciem Preact",
"setup_create_sample_html": "Stwórz przykładową notatkę z użyciem HTML",
"setup_sample_created": "Utworzono przykładową notatkę jako notatkę podrzędną.",
"disabled_description": "Ta notatka pochodzi z zewnętrznego źródła. Ze względów bezpieczeństwa funkcja ta nie jest domyślnie włączona. Upewnij się, że ufasz źródłu, zanim ją aktywujesz.",
"disabled_button_enable": "Włącz renderowanie notatki"
},
"web_view_setup": {
"title": "Utwórz podgląd strony na żywo bezpośrednio w Trilium",
"url_placeholder": "Wpisz lub wklej adres strony internetowej, na przykład https://triliumnotes.org",
"create_button": "Utwórz widok strony",
"invalid_url_title": "Nieprawidłowy adres",
"invalid_url_message": "Wprowadź prawidłowy adres strony, na przykład https://triliumnotes.org.",
"disabled_description": "Ten widok strony został zaimportowany z zewnętrznego źródła. Aby chronić Cię przed phishingiem lub szkodliwą zawartością, nie jest on ładowany automatycznie. Możesz go włączyć, jeśli ufasz źródłu.",
"disabled_button_enable": "Włącz widok strony"
},
"active_content_badges": {
"type_icon_pack": "Pakiet ikon",
"type_backend_script": "Skrypt po stronie serwera",
"type_frontend_script": "Skrypt po stronie klienta",
"type_widget": "Widżet",
"type_app_css": "Niestandardowy CSS",
"type_render_note": "Renderuj notatkę",
"type_web_view": "Widok strony",
"type_app_theme": "Własny motyw",
"toggle_tooltip_enable_tooltip": "Kliknij, aby włączyć {{type}}.",
"toggle_tooltip_disable_tooltip": "Kliknij, aby wyłączyć {{type}}.",
"menu_docs": "Otwórz dokumentację",
"menu_execute_now": "Uruchom skrypt teraz",
"menu_run": "Uruchamiaj automatycznie",
"menu_run_disabled": "Ręcznie",
"menu_run_backend_startup": "Podczas uruchamiania backendu",
"menu_run_hourly": "Co godzinę",
"menu_run_daily": "Codziennie",
"menu_run_frontend_startup": "Podczas uruchamiania desktopowego frontendu",
"menu_run_mobile_startup": "Podczas uruchamiania mobilnego frontendu",
"menu_change_to_widget": "Zmień na widżet",
"menu_change_to_frontend_script": "Zmień na skrypt frontendowy",
"menu_theme_base": "Baza motywu"
},
"setup_form": {
"more_info": "Dowiedz się więcej"
}
}

View File

@ -1774,6 +1774,7 @@
"add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário",
"cut": "Cortar",
"copy": "Copiar",
"copy-as-markdown": "Copiar como markdown",
"copy-link": "Copiar ligação",
"paste": "Colar",
"paste-as-plain-text": "Colar como texto sem formatação",

View File

@ -1637,6 +1637,7 @@
"add-term-to-dictionary": "Adicionar \"{{term}}\" ao dicionário",
"cut": "Cortar",
"copy": "Copiar",
"copy-as-markdown": "Copiar como markdown",
"copy-link": "Copiar link",
"paste": "Colar",
"paste-as-plain-text": "Colar como texto sem formatação",

View File

@ -1724,6 +1724,7 @@
"electron_context_menu": {
"add-term-to-dictionary": "Adaugă „{{term}}” în dicționar",
"copy": "Copiază",
"copy-as-markdown": "Copiază ca markdown",
"copy-link": "Copiază legătura",
"cut": "Decupează",
"paste": "Lipește",

View File

@ -708,6 +708,7 @@
"paste": "Вставить",
"copy-link": "Скопировать ссылку",
"copy": "Скопировать",
"copy-as-markdown": "Копировать как Markdown",
"cut": "Вырезать",
"search_online": "Поиск \"{{term}}\" в {{searchEngine}}",
"add-term-to-dictionary": "Добавить \"{{term}}\" в словарь",

View File

@ -1722,6 +1722,7 @@
"add-term-to-dictionary": "將 \"{{term}}\" 新增至字典",
"cut": "剪下",
"copy": "複製",
"copy-as-markdown": "複製為 Markdown",
"copy-link": "複製連結",
"paste": "貼上",
"paste-as-plain-text": "以純文字貼上",

View File

@ -1533,6 +1533,7 @@
"add-term-to-dictionary": "Додати \"{{term}}\" до словника",
"cut": "Вирізати",
"copy": "Копіювати",
"copy-as-markdown": "Копіювати як Markdown",
"copy-link": "Копіювати посилання",
"paste": "Вставити",
"paste-as-plain-text": "Вставити як звичайний текст",

View File

@ -14,7 +14,7 @@
height: 100%;
display: flex;
gap: 1em;
padding-inline: 12px;
margin-inline: var(--content-margin-inline);
padding-block: 4px;
align-items: flex-start;
overflow-x: auto;

View File

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { buildNote, buildNotes } from "../../../test/easy-froca.js";
import { buildEvent, buildEvents } from "./event_builder.js";
import { LOCALE_MAPPINGS } from "./index.js";
@ -148,7 +148,7 @@ describe("Promoted attributes", () => {
expect(event).toHaveLength(1);
expect(event[0]?.promotedAttributes).toMatchObject([
[ "assignee", "Target note" ]
])
]);
});
it("supports start time and end time", async () => {
@ -177,6 +177,86 @@ describe("Promoted attributes", () => {
});
describe("Recurrence", () => {
it("supports valid recurrence without end date", async () => {
const noteIds = buildNotes([
{
title: "Recurring Event",
"#startDate": "2025-05-05",
"#recurrence": "FREQ=DAILY;COUNT=5"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
title: "Recurring Event",
start: "2025-05-05",
});
expect(events[0].rrule).toContain("DTSTART:20250505");
expect(events[0].rrule).toContain("FREQ=DAILY;COUNT=5");
expect(events[0].end).toBeUndefined();
});
it("supports recurrence with start and end time (duration calculated)", async () => {
const noteIds = buildNotes([
{
title: "Timed Recurring Event",
"#startDate": "2025-05-05",
"#startTime": "13:00",
"#endTime": "15:30",
"#recurrence": "FREQ=WEEKLY;COUNT=3"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0]).toMatchObject({
title: "Timed Recurring Event",
start: "2025-05-05T13:00:00",
duration: "02:30"
});
expect(events[0].rrule).toContain("DTSTART:20250505T130000");
expect(events[0].end).toBeUndefined();
});
it("removes end date when recurrence is valid", async () => {
const noteIds = buildNotes([
{
title: "Recurring With End",
"#startDate": "2025-05-05",
"#endDate": "2025-05-07",
"#recurrence": "FREQ=DAILY;COUNT=2"
}
]);
const events = await buildEvents(noteIds);
expect(events).toHaveLength(1);
expect(events[0].rrule).toBeDefined();
expect(events[0].end).toBeUndefined();
});
it("writes to console on invalid recurrence rule", async () => {
const noteIds = buildNotes([
{
title: "Invalid Recurrence",
"#startDate": "2025-05-05",
"#recurrence": "RRULE:FREQ=INVALID"
}
]);
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await buildEvents(noteIds);
const calledWithInvalid = consoleSpy.mock.calls.some(call =>
call[0].includes("has an invalid #recurrence string")
);
expect(calledWithInvalid).toBe(true);
consoleSpy.mockRestore();
});
});
describe("Building locales", () => {
it("every language has a locale defined", async () => {
for (const { id, contentOnly } of LOCALES) {

View File

@ -1,17 +1,22 @@
import { EventInput, EventSourceFuncArg, EventSourceInput } from "@fullcalendar/core/index.js";
import { dayjs } from "@triliumnext/commons";
import clsx from "clsx";
import { start } from "repl";
import * as rruleLib from 'rrule';
import FNote from "../../../entities/fnote";
import froca from "../../../services/froca";
import server from "../../../services/server";
import { formatDateToLocalISO, getCustomisableLabel, getMonthsInDateRange, offsetDate } from "./utils";
import toastService from "../../../services/toast";
import { getCustomisableLabel, getMonthsInDateRange } from "./utils";
interface Event {
startDate: string,
endDate?: string | null,
startTime?: string | null,
endTime?: string | null,
isArchived?: boolean;
isArchived?: boolean,
recurrence?: string | null;
}
export async function buildEvents(noteIds: string[]) {
@ -28,8 +33,17 @@ export async function buildEvents(noteIds: string[]) {
const endDate = getCustomisableLabel(note, "endDate", "calendar:endDate");
const startTime = getCustomisableLabel(note, "startTime", "calendar:startTime");
const endTime = getCustomisableLabel(note, "endTime", "calendar:endTime");
const recurrence = getCustomisableLabel(note, "recurrence", "calendar:recurrence");
const isArchived = note.hasLabel("archived");
events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, isArchived }));
try {
events.push(await buildEvent(note, { startDate, endDate, startTime, endTime, recurrence, isArchived }));
} catch (error) {
if (error instanceof Error) {
const errorMessage = error.message;
toastService.showError(errorMessage);
console.error(errorMessage);
}
}
}
return events.flat();
@ -59,6 +73,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
events.push(await buildEvent(dateNote, { startDate }));
if (dateNote.hasChildren()) {
const childNoteIds = await dateNote.getSubtreeNoteIds();
for (const childNoteId of childNoteIds) {
@ -79,7 +94,7 @@ export async function buildEventsForCalendar(note: FNote, e: EventSourceFuncArg)
return events.flat();
}
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, isArchived }: Event) {
export async function buildEvent(note: FNote, { startDate, endDate, startTime, endTime, recurrence, isArchived }: Event) {
const customTitleAttributeName = note.getLabelValue("calendar:title");
const titles = await parseCustomTitle(customTitleAttributeName, note);
const colorClass = note.getColorClass();
@ -98,9 +113,10 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
startDate = (startTime ? `${startDate}T${startTime}:00` : startDate);
if (!startTime) {
const endDateOffset = offsetDate(endDate ?? startDate, 1);
if (endDateOffset) {
endDate = formatDateToLocalISO(endDateOffset);
if (endDate) {
endDate = dayjs(endDate).add(1, "day").format("YYYY-MM-DD");
} else if (startDate) {
endDate = dayjs(startDate).add(1, "day").format("YYYY-MM-DD");
}
}
@ -118,6 +134,30 @@ export async function buildEvent(note: FNote, { startDate, endDate, startTime, e
if (endDate) {
eventData.end = endDate;
}
if (recurrence) {
// Generate rrule string
const rruleString = `DTSTART:${dayjs(startDate).format("YYYYMMDD[T]HHmmss")}\n${recurrence}`;
// Validate rrule string
let rruleValid = true;
try {
rruleLib.rrulestr(rruleString, { forceset: true }) as rruleLib.RRuleSet;
} catch {
rruleValid = false;
}
if (rruleValid) {
delete eventData.end;
eventData.rrule = rruleString;
if (endDate){
const duration = dayjs.duration(dayjs(endDate).diff(dayjs(startDate)));
eventData.duration = duration.format("HH:mm");
}
} else {
throw new Error(`Note "${note.noteId} ${note.title}" has an invalid #recurrence string ${recurrence}. Excluding...`);
}
}
events.push(eventData);
}
return events;

View File

@ -52,7 +52,7 @@
--fc-border-color: var(--main-border-color);
--fc-neutral-bg-color: var(--launcher-pane-background-color);
--fc-list-event-hover-bg-color: var(--left-pane-item-hover-background);
padding: 0 12px;
padding: 0 var(--content-margin-inline);
}
.calendar-container .fc-list-sticky .fc-list-day > * {

View File

@ -252,6 +252,7 @@ function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
plugins.push((await import("@fullcalendar/timegrid")).default);
plugins.push((await import("@fullcalendar/list")).default);
plugins.push((await import("@fullcalendar/multimonth")).default);
plugins.push((await import("@fullcalendar/rrule")).default);
if (isEditable || isCalendarRoot) {
plugins.push((await import("@fullcalendar/interaction")).default);
}

View File

@ -234,7 +234,7 @@
}
&.type-pdf {
iframe {
div {
height: 50vh;
}

View File

@ -13,6 +13,7 @@
.table-view-container {
height: 100%;
margin-inline-start: var(--content-margin-inline);
}
.search-result-widget-content .table-view {

View File

@ -35,7 +35,7 @@
.calendar-dropdown-widget .calendar-header {
align-items: center;
display: flex;
padding: 0 0.5rem 0.5rem 0.5rem;
padding: 0 0.5rem 1rem 0.5rem;
}
.calendar-dropdown-widget .calendar-header>div {
@ -65,8 +65,13 @@
border: 0;
border-inline-start: unset;
background-color: var(--menu-background-color);
font-weight: bold;
outline: 0;
font-weight: 300;
font-size: 1.4em;
}
.calendar-dropdown-widget .calendar-header .dropdown-toggle {
justify-content: center;
}
.calendar-dropdown-widget .calendar-header .dropdown-toggle::after {
@ -82,18 +87,20 @@
.calendar-dropdown-widget .calendar-week span {
flex-direction: column;
flex: 0 0 12.5%;
font-size: 1rem;
font-weight: bold;
max-width: 12.5%;
padding-top: 5px;
padding-bottom: 5px;
text-align: center;
text-transform: uppercase;
font-size: .85em;
font-weight: 500;
}
.calendar-dropdown-widget .calendar-body {
display: flex;
flex-wrap: wrap;
min-height: 250px;
align-items: center;
}
.calendar-dropdown-widget .calendar-week-number {

View File

@ -4,10 +4,11 @@ body.experimental-feature-new-layout {
}
.title-actions {
--title-actions-padding-start: 12px;
--title-actions-padding-end: 8px;
--title-actions-padding-start: var(--content-margin-inline);
--title-actions-padding-end: var(--content-margin-inline);
display: flex;
width: 100%;
max-width: var(--max-content-width);
flex-direction: column;
gap: 0.5em;

View File

@ -11,7 +11,6 @@
position: relative;
top: 5px;
padding: .25em 0;
display: flex;
align-items: center;
overflow-x: auto;

View File

@ -17,11 +17,13 @@ body.experimental-feature-new-layout .note-paths-widget {
padding: 8px 20px 8px 25px;
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
}
& + li {

View File

@ -4,8 +4,8 @@
contain: none !important;
}
.search-result-widget .note-list {
padding: 10px;
.search-result-widget .note-list-wrapper {
margin-inline: var(--content-margin-inline);
}
.note-split.type-search .scrolling-container {

View File

@ -3,4 +3,8 @@
margin-inline: 40px;
flex-direction: column;
justify-content: center;
label {
margin-bottom: 8px;
}
}

View File

@ -1,14 +1,16 @@
import type { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import Inter from "./../../../fonts/Inter/Inter-VariableFont_opsz,wght.ttf";
import { useSyncedRef, useTriliumOption, useTriliumOptionBool } from "../../react/hooks";
const VARIABLE_WHITELIST = new Set([
"root-background",
"main-background-color",
"main-border-color",
"main-text-color"
]);
interface FontDefinition {
name: string;
url: string;
}
const FONTS: FontDefinition[] = [
{name: "Inter", url: Inter},
]
interface PdfViewerProps extends Pick<HTMLAttributes<HTMLIFrameElement>, "tabIndex"> {
iframeRef?: RefObject<HTMLIFrameElement>;
@ -34,6 +36,7 @@ export default function PdfViewer({ iframeRef: externalIframeRef, pdfUrl, onLoad
<iframe
ref={iframeRef}
class="pdf-preview"
style={{width: "100%", height: "100%"}}
src={`pdfjs/web/viewer.html?file=${pdfUrl}&lang=${locale}&sidebar=${newLayout ? "0" : "1"}&editable=${editable ? "1" : "0"}`}
onLoad={() => {
injectStyles();
@ -55,8 +58,12 @@ function useStyleInjection(iframeRef: RefObject<HTMLIFrameElement>) {
style.id = 'client-root-vars';
style.textContent = cssVarsToString(getRootCssVariables());
styleRef.current = style;
doc.head.appendChild(style);
const fontStyles = doc.createElement("style");
fontStyles.textContent = FONTS.map(injectFont).join("\n");
doc.head.appendChild(fontStyles);
}, [ iframeRef ]);
// React to changes.
@ -79,7 +86,7 @@ function getRootCssVariables() {
for (let i = 0; i < styles.length; i++) {
const prop = styles[i];
if (prop.startsWith('--') && VARIABLE_WHITELIST.has(prop.substring(2))) {
if (prop.startsWith('--')) {
vars[`--tn-${prop.substring(2)}`] = styles.getPropertyValue(prop).trim();
}
}
@ -92,3 +99,12 @@ function cssVarsToString(vars: Record<string, string>) {
.map(([k, v]) => ` ${k}: ${v};`)
.join('\n')}\n}`;
}
function injectFont(font: FontDefinition) {
return `
@font-face {
font-family: '${font.name}';
src: url('${font.url}');
}
`;
}

View File

@ -19,4 +19,12 @@
.tn-link {
margin-top: 1em;
}
label {
margin-bottom: 8px;
}
.input-group {
margin: 0;
}
}

View File

@ -3,5 +3,5 @@
DIR=`dirname "$0"`
export NODE_TLS_REJECT_UNAUTHORIZED=0
"$DIR/trilium"
exec "$DIR/trilium"

View File

@ -3,5 +3,5 @@
DIR=`dirname "$0"`
export TRILIUM_DATA_DIR="$DIR/trilium-data"
"$DIR/trilium"
exec "$DIR/trilium"

View File

@ -3,5 +3,5 @@
DIR=`dirname "$0"`
export TRILIUM_SAFE_MODE=1
"$DIR/trilium" --disable-gpu
exec "$DIR/trilium" --disable-gpu

View File

@ -69,7 +69,7 @@
"@types/xml2js": "0.4.14",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"axios": "1.13.5",
"axios": "1.13.6",
"bindings": "1.5.0",
"bootstrap": "5.3.8",
"chardet": "2.1.1",
@ -108,7 +108,7 @@
"lorem-ipsum": "2.0.8",
"marked": "17.0.3",
"mime-types": "3.0.2",
"multer": "2.0.2",
"multer": "2.1.0",
"normalize-strings": "1.1.1",
"rand-token": "1.0.1",
"safe-compare": "1.1.4",

View File

@ -43,7 +43,7 @@ rm -rf $BUILD_DIR/node/lib/node_modules/{npm,corepack} \
$BUILD_DIR/node_modules/electron* \
$BUILD_DIR/electron*.{js,map}
printf "#!/bin/sh\n./node/bin/node main.cjs\n" > $BUILD_DIR/trilium.sh
printf "#!/bin/sh\nexec ./node/bin/node main.cjs\n" > $BUILD_DIR/trilium.sh
chmod 755 $BUILD_DIR/trilium.sh
VERSION=`jq -r ".version" package.json`

View File

@ -185,6 +185,18 @@
at which the event ends (in relation with <code spellcheck="false">endDate</code> if
present, or <code spellcheck="false">startDate</code>).</td>
</tr>
<tr>
<td><code spellcheck="false">#recurrence</code>
</td>
<td>This is an optional CalDAV <code spellcheck="false">RRULE</code> string
that if present, determines whether a task should repeat or not. Note that
it does not include the <code spellcheck="false">DTSTART</code> attribute,
which is derived from the <code spellcheck="false">#startDate</code> and
<code
spellcheck="false">#startTime</code>directly. For examples of valid <code spellcheck="false">RRULE</code> strings
see <a href="https://icalendar.org/rrule-tool.html">https://icalendar.org/rrule-tool.html</a>
</td>
</tr>
<tr>
<td><code spellcheck="false">#color</code>
</td>
@ -282,6 +294,65 @@
<p>When not used in a Journal, the calendar is recursive. That is, it will
look for events not just in its child notes but also in the children of
these child notes.</p>
<p>&nbsp;</p>
<h2>Recurrence</h2>
<p>The built in calendar view also supports repeating tasks. If a child note
of the calendar has a #recurrence label with a valid recurrence, that event
will repeat on the calendar according to the recurrence string.&nbsp;</p>
<p>For example, to make a note repeat on the calendar:</p>
<ul>
<li>
<p>Every Day - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=1"</code>
</p>
</li>
<li>
<p>Every 3 days - <code spellcheck="false">#recurrence="FREQ=DAILY;INTERVAL=3"</code>
</p>
</li>
<li>
<p>Every week - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=1"</code>
</p>
</li>
<li>
<p>Every 2 weeks on Monday, Wednesday and Friday - <code spellcheck="false">#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"</code>
</p>
</li>
<li>
<p>Every 3 months - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=3"</code>
</p>
</li>
<li>
<p>Every 2 months on the First Sunday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"</code>
</p>
</li>
<li>
<p>Every month on the Last Friday - <code spellcheck="false">#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"</code>
</p>
</li>
<li>
<p>And so on.</p>
</li>
</ul>
<p>For other examples of valid <code spellcheck="false">RRULE</code> strings
see <a href="https://icalendar.org/rrule-tool.html">https://icalendar.org/rrule-tool.html</a>
</p>
<p>Note that the recurrence string does not include the <code spellcheck="false">DTSTART</code> attribute
as defined in the iCAL specifications. This is derived directly from the
<code
spellcheck="false">startDate</code>and <code spellcheck="false">startTime</code> attributes</p>
<p>If you want to override the label the calendar uses to fetch the recurrence
string, you can use the <code spellcheck="false">#calendar:recurrence</code> attribute.
For example, you can set <code spellcheck="false">#calendar:recurrence=taskRepeats</code>.
Then you can set your recurrence string like <code spellcheck="false">#taskRepeats="FREQ=DAILY;INTERVAL=1"</code>
</p>
<p>Also note that the recurrence label can be made promoted as with the start
and end dates.&nbsp;</p>
<aside class="admonition warning">
<p>If the recurrence string is not valid, a toast will be shown with the
note ID and title of the note with the erroneous recurrence message. This
note will not be added to the calendar</p>
</aside>
<p>&nbsp;</p>
<h2>Use-cases</h2>
<h3>Using with the Journal / calendar</h3>
<p>It is possible to integrate the calendar view into the Journal with day

View File

@ -13,7 +13,7 @@
"postinstall": "wxt prepare"
},
"keywords": [],
"packageManager": "pnpm@10.30.2",
"packageManager": "pnpm@10.30.3",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.18"

View File

@ -72,11 +72,12 @@ For each note of the calendar, the following attributes can be used:
| `#endDate` | Similar to `startDate`, mentions the end date if the event spans across multiple days. The date is inclusive, so the end day is also considered. The attribute can be missing for single-day events. |
| `#startTime` | The time the event starts at. If this value is missing, then the event is considered a full-day event. The format is `HH:MM` (hours in 24-hour format and minutes). |
| `#endTime` | Similar to `startTime`, it mentions the time at which the event ends (in relation with `endDate` if present, or `startDate`). |
| `#recurrence` | This is an optional CalDAV `RRULE` string that if present, determines whether a task should repeat or not. Note that it does not include the `DTSTART` attribute, which is derived from the `#startDate` and `#startTime` directly. For examples of valid `RRULE` strings see [https://icalendar.org/rrule-tool.html](https://icalendar.org/rrule-tool.html) |
| `#color` | Displays the event with a specified color (named such as `red`, `gray` or hex such as `#FF0000`). This will also change the color of the note in other places such as the note tree. |
| `#calendar:color` | **❌️ Removed since v0.100.0. Use** `**#color**` **instead.**  <br> <br>Similar to `#color`, but applies the color only for the event in the calendar and not for other places such as the note tree. |
| `#calendar:color` | **❌️ Removed since v0.100.0. Use** `**#color**` **instead.**   <br> <br>Similar to `#color`, but applies the color only for the event in the calendar and not for other places such as the note tree. |
| `#iconClass` | If present, the icon of the note will be displayed to the left of the event title. |
| `#calendar:title` | Changes the title of an event to point to an attribute of the note other than the title, can either a label or a relation (without the `#` or `~` symbol). See _Use-cases_ for more information. |
| `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this:       <br> <br>![](7_Calendar_image.png)      <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"`     <br> <br>It can also be used with relations, case in which it will display the title of the target note:      <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` |
| `#calendar:displayedAttributes` | Allows displaying the value of one or more attributes in the calendar like this:        <br> <br>![](7_Calendar_image.png)       <br> <br>`#weight="70" #Mood="Good" #calendar:displayedAttributes="weight,Mood"`      <br> <br>It can also be used with relations, case in which it will display the title of the target note:       <br> <br>`~assignee=@My assignee #calendar:displayedAttributes="assignee"` |
| `#calendar:startDate` | Allows using a different label to represent the start date, other than `startDate` (e.g. `expiryDate`). The label name **must not be** prefixed with `#`. If the label is not defined for a note, the default will be used instead. |
| `#calendar:endDate` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the end date. |
| `#calendar:startTime` | Similar to `#calendar:startDate`, allows changing the attribute which is being used to read the start time. |
@ -102,6 +103,32 @@ This will result in:
When not used in a Journal, the calendar is recursive. That is, it will look for events not just in its child notes but also in the children of these child notes.
## Recurrence
The built in calendar view also supports repeating tasks. If a child note of the calendar has a #recurrence label with a valid recurrence, that event will repeat on the calendar according to the recurrence string. 
For example, to make a note repeat on the calendar:
* Every Day - `#recurrence="FREQ=DAILY;INTERVAL=1"`
* Every 3 days - `#recurrence="FREQ=DAILY;INTERVAL=3"`
* Every week - `#recurrence="FREQ=WEEKLY;INTERVAL=1"`
* Every 2 weeks on Monday, Wednesday and Friday - `#recurrence="FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR"`
* Every 3 months - `#recurrence="FREQ=MONTHLY;INTERVAL=3"`
* Every 2 months on the First Sunday - `#recurrence="FREQ=MONTHLY;INTERVAL=2;BYDAY=1SU"`
* Every month on the Last Friday - `#recurrence="FREQ=MONTHLY;INTERVAL=1;BYDAY=-1FR"`
* And so on.
For other examples of valid `RRULE` strings see [https://icalendar.org/rrule-tool.html](https://icalendar.org/rrule-tool.html)
Note that the recurrence string does not include the `DTSTART` attribute as defined in the iCAL specifications. This is derived directly from the `startDate` and `startTime` attributes
If you want to override the label the calendar uses to fetch the recurrence string, you can use the `#calendar:recurrence` attribute. For example, you can set `#calendar:recurrence=taskRepeats`. Then you can set your recurrence string like `#taskRepeats="FREQ=DAILY;INTERVAL=1"`
Also note that the recurrence label can be made promoted as with the start and end dates. 
> [!WARNING]
> If the recurrence string is not valid, a toast will be shown with the note ID and title of the note with the erroneous recurrence message. This note will not be added to the calendar
## Use-cases
### Using with the Journal / calendar

View File

@ -50,7 +50,7 @@
"@triliumnext/server": "workspace:*",
"@types/express": "5.0.6",
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.14",
"@types/node": "24.11.0",
"@vitest/browser-webdriverio": "4.0.18",
"@vitest/coverage-v8": "4.0.18",
"@vitest/ui": "4.0.18",
@ -61,7 +61,7 @@
"eslint": "10.0.2",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.7.1",
"eslint-plugin-playwright": "2.8.0",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.7.0",
"http-server": "14.1.1",
@ -93,7 +93,7 @@
"url": "https://github.com/TriliumNext/Trilium/issues"
},
"homepage": "https://triliumnotes.org",
"packageManager": "pnpm@10.30.2",
"packageManager": "pnpm@10.30.3",
"pnpm": {
"patchedDependencies": {
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",

View File

@ -47,7 +47,7 @@
"@ssddanbrown/codemirror-lang-smarty": "1.0.0",
"@ssddanbrown/codemirror-lang-twig": "1.0.0",
"@triliumnext/commons": "workspace:*",
"codemirror-lang-elixir": "4.0.0",
"codemirror-lang-elixir": "4.0.1",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "10.0.2"

View File

@ -1,31 +1,332 @@
:root {
color-scheme: var(--tn-theme-style);
/* #region General */
--body-bg-color: transparent;
--toolbar-bg-color: transparent;
:root {
--main-color: var(--tn-main-text-color);
--body-bg-color: transparent;
--outline-color: gray;
--focus-ring-color: var(--tn-input-focus-outline-color);
--toolbar-border-color: var(--tn-main-border-color);
--toolbar-icon-bg-color: var(--tn-main-text-color);
--toolbar-bg-color: transparent;
--toolbar-icon-opacity: 1;
--toggled-btn-bg-color: var(--tn-hover-item-background-color);
--doorhanger-bg-color: var(--tn-menu-background-color);
--doorhanger-separator-color: var(--tn-main-border-color);
--page-margin: 12px auto;
--spreadHorizontalWrapped-margin-LR: 4px;
color-scheme: var(--tn-theme-style);
font-size: 16px;
}
.pdfViewer {
.page,
.page > .canvasWrapper,
.page > .canvasWrapper > canvas {
border-radius: 6px;
}
.page {
border: 1px solid var(--tn-main-border-color);
box-shadow: 7px 7px 15px #00000010;
body.read-only-document {
/* TODO: find a more elegant way to display a PDF in a read only view */
--toolbar-height: 0;
.toolbar,
.editToolbar {
display: none !important;
}
}
:root button,
:root dialog,
:root #toolbarContainer,
:root .toolbarButton,
:root #scaleSelect,
:root .toolbarButtonWithContainer .editorParamsToolbar .editorParamsLabel,
:root #toolbarContainer #toolbarViewer input,
:root #editorUndoBar,
:root .dialogButton {
font-family: Inter, sans-serif;
}
#secondaryToolbar,
#documentPropertiesDialog,
#findbar.doorHanger,
.doorHangerRight,
#printServiceDialog,
:root :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor, .signatureEditor), .textLayer) .editToolbar,
#viewerContainer .editToolbar .colorPicker .dropdown,
#editorUndoBar {
border: 1px solid var(--tn-dropdown-border-color);
border-radius: var(--tn-dropdown-border-radius);
background-color: var(--tn-menu-background-color);
padding: var(--tn-menu-padding-size);
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--tn-dropdown-shadow-opacity));
backdrop-filter: var(--tn-dropdown-backdrop-filter);
}
.doorHangerRight,
.doorHangerLeft,
.doorHanger {
&::after, &::before {
display: none;
}
}
:root .toggle-button {
--toggle-border-color: transparent;
--toggle-background-color: var(--tn-input-background-color);
--toggle-background-color-hover: var(--toggle-background-color);
--toggle-dot-background-color: var(--tn-input-text-color);
--toggle-background-color-pressed: var(--tn-input-text-color);
--toggle-background-color-pressed-hover: var(--toggle-background-color-pressed);
cursor: pointer;
}
:root .colorPicker {
--hover-outline-color: var(--tn-input-focus-outline-color);
--selected-outline-color: var(--tn-main-text-color);
outline-offset: 3px;
}
/* Text boxes */
input:not([type]),
input[type="number"] {
--field-border-color: transparent;
--field-bg-color: var(--tn-input-background-color);
--field-color: var(--tn-input-text-color);
--input-horizontal-padding: 8px;
border-radius: 4px !important;
font-size: .85rem !important;
&:hover {
--field-bg-color: var(--tn-input-hover-background);
--field-color: var(--tn-input-hover-color);
}
&:focus {
border-color: transparent !important;
outline: 2px solid var(--tn-input-focus-outline-color);
--field-bg-color: var(--tn-input-focus-background);
--field-color: var(--tn-input-focus-color);
}
&::placeholder {
color: var(--tn-input-placeholder-color)
}
&::selection {
background-color: var(--tn-input-selection-background);
color: var(--tn-input-selection-text-color);
}
}
input[type="color"] {
border: 0;
outline: 0;
background: transparent;
border-radius: 4px;
&:hover {
opacity: .75;
}
&:focus {
outline: 2px solid var(--tn-input-focus-outline-color);
outline-offset: 2px;
}
}
/* #endregion */
/* #region Toolbar */
#toolbarContainer select.scaleSelect,
#toolbarContainer input.pageNumber {
height: calc(var(--toolbar-height) - 8px);
padding-block: 0;
}
#toolbarContainer {
padding-inline: 12px;
}
.toolbarButton {
border-radius: 6px;
&:not(.labeled):active::before {
transform: scale(.85) !important;
}
&:hover {
background: var(--tn-hover-item-background-color);
color: var(--tn-hover-item-text-color);
}
&.toggled {
/* Icon-only button */
&:not(.labeled) {
background: var(--tn-ck-editor-toolbar-button-on-background);
&&::before {
background: var(--tn-ck-editor-toolbar-button-on-color);
}
}
&::before {
color: var(--tn-menu-item-icon-color);
}
}
}
.verticalToolbarSeparator,
.splitToolbarButtonSeparator {
--separator-color: transparent;
}
.verticalToolbarSeparator {
margin-inline: 4px;
}
:root #findbar {
--toolbar-height: 40px;
padding: 0 4px;
/* Search input */
.loadingInput {
margin-inline-end: 8px;
}
/* Search input - no results */
#findInputContainer #findInput[data-status="notFound"] {
--tn-input-focus-outline-color: var(--tn-dropdown-item-icon-destructive-color);
background: inherit;
}
/* Option buttons */
.toggleButton.toolbarLabel,
.toolbarButton {
height: calc(var(--toolbar-height) - 12px);
padding-inline: 10px;
border-radius: 6px;
aspect-ratio: unset;
}
/* Toggable option buttons */
.toggleButton.toolbarLabel {
--main-color: var(--tn-main-text-color);
--button-hover-color: var(--tn-hover-item-background-color);
--toggled-btn-bg-color: var(--tn-ck-editor-toolbar-button-on-background);
--toggled-btn-color: var(--tn-ck-editor-toolbar-button-on-color);
}
/* Search status text */
#findbarMessageContainer #findResultsCount,
#findMsg {
background-color: transparent;
color: var(--tn-main-text-color);
opacity: .5;
};
/* Not found message */
#findMsg[data-status="notFound"] {
color: var(--tn-dropdown-item-icon-destructive-color);
opacity: 1;
}
}
#toolbarContainer #toolbarViewer #pageNumber {
font-size: 12px;
font-weight: 600;
}
#scaleSelectContainer {
--dropdown-btn-bg-color: transparent;
--button-hover-color: transparent;
border-radius: 6px;
&:hover,
&:focus-within{
background-color: var(--tn-hover-item-background-color);
}
}
/* Toolbar editor dropdowns */
:root .editorParamsToolbar:not(.menu),
:root #highlightParamsToolbarContainer {
padding: 10px 16px;
}
/* Toolbar dropdowns */
:root .editorParamsToolbar {
.menu {
padding: 8px;
}
.editorParamsToolbarContainer {
padding: 0;
}
}
/* Overflow menu */
:root #secondaryToolbar {
--toolbar-icon-bg-color: var(--tn-menu-item-icon-color);
--toolbar-icon-hover-bg-color: var(--tn-menu-item-icon-color);
--toggled-btn-bg-color: transparent;
--toggled-btn-color: currentColor;
--doorhanger-icon-opacity: 1;
padding: var(--tn-menu-padding-size);
width: auto;
min-width: 220px;
max-width: 400px;
.toolbarButton.labeled {
color: var(--tn-menu-text-color);
padding-inline-end: 12px;
padding-block: 6px;
font-size: .9rem;
}
}
/* Horizontal menu dividers */
:root #highlightParamsToolbarContainer #editorHighlightVisibility .divider,
:root .horizontalToolbarSeparator {
position: relative;
overflow: visible;
background: unset;
border: none;
&:before {
content: "";
position: absolute;
left: 0;
right: 0;
border-top: 1px solid var(--tn-main-border-color);
}
}
/* Radio menu items */
#cursorToolButtons .toolbarButton,
#scrollModeButtons .toolbarButton,
#spreadModeButtons .toolbarButton {
--toggled-hover-active-btn-color: var(--tn-hover-item-background-color);
position: relative;
&::after {
display: block;
content: "";
position: absolute;
right: 0;
width: 2em;
height: 100%;
/* https://pictogrammers.com/library/mdi/icon/radiobox-blank/ */
mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3ctitle%3eradiobox-blank%3c/title%3e%3cpath d='M12%2c20A8%2c8 0 0%2c1 4%2c12A8%2c8 0 0%2c1 12%2c4A8%2c8 0 0%2c1 20%2c12A8%2c8 0 0%2c1 12%2c20M12%2c2A10%2c10 0 0%2c0 2%2c12A10%2c10 0 0%2c0 12%2c22A10%2c10 0 0%2c0 22%2c12A10%2c10 0 0%2c0 12%2c2Z' /%3e%3c/svg%3e");
mask-size: 16px;
mask-repeat: no-repeat;
mask-position: center center;
background-color: var(--tn-main-text-color);
}
&.toggled::after {
/* https://pictogrammers.com/library/mdi/icon/radiobox-marked/ */
mask-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3e%3ctitle%3eradiobox-marked%3c/title%3e%3cpath d='M12%2c20A8%2c8 0 0%2c1 4%2c12A8%2c8 0 0%2c1 12%2c4A8%2c8 0 0%2c1 20%2c12A8%2c8 0 0%2c1 12%2c20M12%2c2A10%2c10 0 0%2c0 2%2c12A10%2c10 0 0%2c0 12%2c22A10%2c10 0 0%2c0 22%2c12A10%2c10 0 0%2c0 12%2c2M12%2c7A5%2c5 0 0%2c0 7%2c12A5%2c5 0 0%2c0 12%2c17A5%2c5 0 0%2c0 17%2c12A5%2c5 0 0%2c0 12%2c7Z' /%3e%3c/svg%3e");
}
}
/* Permanently removed buttons */
#viewsManagerToggleButton,
#downloadButton,
@ -43,10 +344,103 @@
/* #region Properties Dialog */
/* Hide irrelevant properties */
#documentPropertiesDialog > .row:has(#fileNameField),
#documentPropertiesDialog > .row:has(#linearizedField) {
display: none;
#documentPropertiesDialog {
--separator-color: transparent;
user-select: none;
padding: 1em;
.row {
line-height: 1.5;
> span {
font-weight: bold;
opacity: .5;
}
> p {
user-select: all;
}
}
/* Hide irrelevant properties */
> .row:has(#fileNameField),
> .row:has(#linearizedField) {
display: none;
}
#documentPropertiesClose {
/* TODO: restyle */
border-radius: 6px;
cursor: pointer;
}
}
/* #endregion */
/* #region Viewer Area */
.pdfViewer {
.page,
.page > .canvasWrapper,
.page > .canvasWrapper > canvas {
border-radius: 6px;
}
.page {
border: 1px solid var(--tn-main-border-color);
box-shadow: 7px 7px 15px #00000010;
}
}
#viewsManager {
display: none;
}
:root :is(.annotationEditorLayer :is(.freeTextEditor, .inkEditor, .stampEditor, .highlightEditor, .signatureEditor), .textLayer) .editToolbar {
--editor-toolbar-hover-bg-color: var(--tn-hover-item-background-color);
--editor-toolbar-hover-fg-color: var(--tn-hover-item-text-color);
--editor-toolbar-hover-outline: transparent;
padding: 4px;
.divider {
display: none;
}
.buttons {
gap: 2px;
> * {
border-radius: 6px !important;
}
.deleteButton::before {
background-color: var(--tn-dropdown-item-icon-destructive-color);
}
}
}
#viewerContainer .editToolbar .colorPicker .dropdown {
padding: 12px 6px;
&::before {
display: block;
position: absolute;
content: "";
inset: 0;
border: 1px solid var(--tn-dropdown-border-color);
border-radius: var(--tn-dropdown-border-radius);
backdrop-filter: var(--tn-dropdown-backdrop-filter);
z-index: -1;
}
}
#editorUndoBar {
--message-bar-fg-color: var(--tn-main-text-color);
--message-bar-icon-color: var(--tn-menu-item-icon-color);
--undo-button-border: transparent;
padding-inline: 20px;
}
/* #endregion */

View File

@ -7,6 +7,9 @@ import { setupPdfLayers } from "./layers";
async function main() {
const urlParams = new URLSearchParams(window.location.search);
const isEditable = urlParams.get("editable") === "1";
document.body.classList.toggle("read-only-document", !isEditable);
if (urlParams.get("sidebar") === "0") {
hideSidebar();
}

388
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff