Merge branch 'main' into exec-in-launcher-scripts

This commit is contained in:
Elian Doran 2026-02-26 20:17:53 +02:00 committed by GitHub
commit 9374694a0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
47 changed files with 1441 additions and 1028 deletions

2
.nvmrc
View File

@ -1 +1 @@
24.13.1
24.14.0

View File

@ -14,9 +14,9 @@
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.30.2",
"devDependencies": {
"@redocly/cli": "2.19.1",
"@redocly/cli": "2.19.2",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"js-yaml": "4.1.1",

View File

@ -56,7 +56,7 @@
"mark.js": "8.11.1",
"marked": "17.0.3",
"mermaid": "11.12.3",
"mind-elixir": "5.9.0",
"mind-elixir": "5.9.1",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.4",
@ -71,7 +71,7 @@
"@ckeditor/ckeditor5-inspector": "5.0.0",
"@prefresh/vite": "2.4.12",
"@types/bootstrap": "5.2.10",
"@types/jquery": "3.5.33",
"@types/jquery": "4.0.0",
"@types/leaflet": "1.9.21",
"@types/leaflet-gpx": "1.3.8",
"@types/mark.js": "8.11.12",

View File

@ -2630,7 +2630,7 @@ iframe.print-iframe {
}
}
#root-widget.virtual-keyboard-opened .note-split:not(.active) {
body:not(.ios) #root-widget.virtual-keyboard-opened .note-split:not(.active) {
max-height: 80px;
opacity: 0.4;
}

View File

@ -1534,7 +1534,8 @@
"task-list": "任务列表",
"new-feature": "新建",
"collections": "集合",
"book": "集合"
"book": "集合",
"ai-chat": "AI聊天"
},
"protect_note": {
"toggle-on": "保护笔记",
@ -1630,7 +1631,8 @@
},
"search_result": {
"no_notes_found": "没有找到符合搜索条件的笔记。",
"search_not_executed": "尚未执行搜索。请点击上方的\"搜索\"按钮查看结果。"
"search_not_executed": "尚未执行搜索。",
"search_now": "立即搜索"
},
"spacer": {
"configure_launchbar": "配置启动栏"
@ -2005,7 +2007,9 @@
"app-restart-required": "(需重启程序以应用更改)"
},
"pagination": {
"total_notes": "{{count}} 篇笔记"
"total_notes": "{{count}} 篇笔记",
"prev_page": "上一页",
"next_page": "下一页"
},
"collections": {
"rendering_error": "出现错误无法显示内容。"

View File

@ -1679,7 +1679,8 @@
},
"search_result": {
"no_notes_found": "Ní bhfuarthas aon nótaí do na paraiméadair chuardaigh tugtha.",
"search_not_executed": "Níl an cuardach curtha i gcrích fós. Cliceáil ar an gcnaipe \"Cuardaigh\" thuas chun na torthaí a fheiceáil."
"search_not_executed": "Níl an cuardach curtha i gcrích fós.",
"search_now": "Cuardaigh anois"
},
"spacer": {
"configure_launchbar": "Cumraigh an Barra Seoladh"

View File

@ -83,7 +83,10 @@
"erase_notes_warning": "Hapus catatan secara permanen (tidak bisa dikembalikan), termasuk semua duplikat. Aksi akan memaksa aplikasi untuk mengulang kembali.",
"notes_to_be_deleted": "Catatan-catatan berikut akan dihapuskan ({{notesCount}})",
"no_note_to_delete": "Tidak ada Catatan yang akan dihapus (hanya duplikat).",
"broken_relations_to_be_deleted": "Hubungan berikut akan diputus dan dihapus ({{ relationCount}})"
"broken_relations_to_be_deleted": "Hubungan berikut akan diputus dan dihapus ({{ relationCount}})",
"cancel": "Batalkan",
"ok": "Setuju",
"deleted_relation_text": "Catatan {{- note}} (yang akan dihapus) dirujuk oleh relasi {{- relation}} yang berasal dari {{- source}}."
},
"clone_to": {
"clone_notes_to": "Duplikat catatan ke…",
@ -96,5 +99,12 @@
"clone_to_selected_note": "Salin ke catatan yang dipilih",
"no_path_to_clone_to": "Tidak ada jalur untuk digandakan.",
"note_cloned": "Catatan \"{{clonedTitle}}\" telah digandakan ke dalam \"{{targetTitle}}\""
},
"search_result": {
"search_now": "Cari sekarang"
},
"export": {
"export_note_title": "Mengeluarkan catatan",
"close": "Tutup"
}
}

View File

@ -118,7 +118,7 @@
"export_type_subtree": "Questa nota e tutti i suoi discendenti",
"format_html": "HTML - raccomandato in quanto mantiene tutti i formati",
"format_html_zip": "HTML in archivio ZIP - questo è raccomandato in quanto conserva tutta la formattazione.",
"format_markdown": "MArkdown - questo conserva la maggior parte della formattazione.",
"format_markdown": "Markdown: preserva la maggior parte della formattazione.",
"export_type_single": "Solo questa nota, senza le sottostanti",
"format_opml": "OPML - formato per scambio informazioni outline. Formattazione, immagini e files non sono inclusi.",
"opml_version_1": "OPML v.1.0 - solo testo semplice",
@ -592,7 +592,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\">finestra di dialogo “Vai a”</a>",
"showJumpToNoteDialog": "mostra <a class=\"external\" href=\"https://triliumnext.github.io/Docs/Wiki/note-navigation.html#jump-to-note\">\"Vai a\"</a>",
"title": "Scheda riassuntiva",
"noteNavigation": "Nota navigazione",
"scrollToActiveNote": "scorri fino alla nota attiva",
@ -1715,7 +1715,8 @@
"beta-feature": "Beta",
"task-list": "Elenco delle attività",
"new-feature": "Nuovo",
"collections": "Collezioni"
"collections": "Collezioni",
"ai-chat": "Chat con IA"
},
"protect_note": {
"toggle-on": "Proteggi la nota",
@ -1793,7 +1794,8 @@
},
"search_result": {
"no_notes_found": "Non sono state trovate note per i parametri di ricerca specificati.",
"search_not_executed": "La ricerca non è stata ancora eseguita. Clicca sul pulsante \"Cerca\" qui sopra per visualizzare i risultati."
"search_not_executed": "La ricerca non è stata ancora eseguita.",
"search_now": "Cerca ora"
},
"spacer": {
"configure_launchbar": "Configura Launchbar"
@ -2020,7 +2022,9 @@
"percentage": "%"
},
"pagination": {
"total_notes": "{{count}} note"
"total_notes": "{{count}} note",
"prev_page": "Pagina precedente",
"next_page": "Pagina successiva"
},
"collections": {
"rendering_error": "Impossibile mostrare il contenuto a causa di un errore."

View File

@ -599,7 +599,8 @@
"beta-feature": "Beta",
"task-list": "タスクリスト",
"new-feature": "New",
"collections": "コレクション"
"collections": "コレクション",
"ai-chat": "AI チャット"
},
"edited_notes": {
"no_edited_notes_found": "この日の編集されたノートはまだありません...",

View File

@ -1,7 +1,6 @@
import "./NoteDetail.css";
import clsx from "clsx";
import { note } from "mermaid/dist/rendering-util/rendering-elements/shapes/note.js";
import { isValidElement, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";

View File

@ -5,7 +5,7 @@
align-items: center;
position: absolute;
width: 100%;
top: 20px;
top: calc(env(safe-area-inset-top) + 20px);
pointer-events: none;
contain: none;
}

View File

@ -9,7 +9,8 @@ import Button from "../react/Button";
import "./Pagination.css";
import clsx from "clsx";
interface PaginationContext {
export interface PaginationContext {
className?: string;
page: number;
setPage: Dispatch<StateUpdater<number>>;
pageNotes?: FNote[];
@ -18,11 +19,11 @@ interface PaginationContext {
totalNotes: number;
}
export function Pager({ page, pageSize, setPage, pageCount, totalNotes }: Omit<PaginationContext, "pageNotes">) {
export function Pager({ className, page, pageSize, setPage, pageCount, totalNotes }: Omit<PaginationContext, "pageNotes">) {
if (pageCount < 2) return;
return (
<div className="note-list-pager-container">
<div className={clsx("note-list-pager-container", className)}>
<div className="note-list-pager">
<ActionButton
icon="bx bx-chevron-left"

View File

@ -13,6 +13,14 @@
flex-wrap: wrap;
gap: 10px;
}
.note-list-bottom-pager {
margin-block: 8px;
}
&:not(:has(.note-list-bottom-pager)) {
margin-bottom: 48px;
}
}
/* #region List view / Grid view common styles */
@ -107,7 +115,7 @@
.nested-note-list .note-book-content,
.note-list-container .note-book-content {
display: none;
animation: note-preview-show .25s ease-out;
animation: note-preview-show .35s ease-out;
will-change: opacity;
&.note-book-content-ready {
@ -347,7 +355,15 @@
.note-book-card .note-book-content {
padding: 0;
flex: 1;
overflow: hidden;
font-size: 0.8rem;
&.note-book-content-overflowing {
mask-image: linear-gradient(to bottom, black calc(100% - 75px), transparent 100%);
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
.ck-content p {
margin-bottom: 0.5em;
@ -358,6 +374,26 @@
width: 25%;
}
.ck-content .table {
display: flex;
flex-direction: column-reverse;
overflow-x: scroll;
--scrollbar-thickness: 0;
scrollbar-width: none;
table {
width: max-content;
table-layout: auto;
}
figcaption {
display: block;
position: sticky;
left: 0;
width: 100%;
}
}
.rendered-content,
.rendered-content.text-with-ellipsis {
padding: .5rem 1rem 1rem 1rem;
@ -368,6 +404,10 @@
padding: 0;
}
&.type-video video {
max-height: 200px;
}
h1, h2, h3, h4, h5, h6 {
font-size: 1rem;
color: var(--active-item-text-color);

View File

@ -13,13 +13,15 @@ import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean,
import Icon from "../../react/Icon";
import NoteLink from "../../react/NoteLink";
import { ViewModeProps } from "../interface";
import { Pager, usePagination } from "../Pagination";
import { Pager, usePagination, PaginationContext } from "../Pagination";
import { filterChildNotes, useFilteredNoteIds } from "./utils";
import { JSX } from "preact/jsx-runtime";
import { clsx } from "clsx";
import ActionButton from "../../react/ActionButton";
import linkContextMenuService from "../../../menus/link_context_menu";
import { TargetedMouseEvent } from "preact";
import { ComponentChildren, TargetedMouseEvent } from "preact";
const contentSizeObserver = new ResizeObserver(onContentResized);
export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
const expandDepth = useExpansionDepth(note);
@ -27,32 +29,18 @@ export function ListView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const noteType = useNoteProperty(note, "type");
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
return (
<div className="note-list list-view">
<CollectionProperties
note={note}
centerChildren={<Pager {...pagination} />}
/>
{ noteIds.length > 0 && <div className="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<Card className={clsx("nested-note-list", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<ListNoteCard
key={childNote.noteId}
note={childNote} parentNote={note}
expandDepth={expandDepth} highlightedTokens={highlightedTokens}
currentLevel={1} includeArchived={includeArchived} />
))}
</Card>
<Pager {...pagination} />
</div>}
</div>
);
return <NoteList note={note} viewMode="list-view" noteIds={noteIds} pagination={pagination}>
<Card className={clsx("nested-note-list", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<ListNoteCard
key={childNote.noteId}
note={childNote} parentNote={note}
expandDepth={expandDepth} highlightedTokens={highlightedTokens}
currentLevel={1} includeArchived={includeArchived} />
))}
</Card>
</NoteList>;
}
export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }: ViewModeProps<{}>) {
@ -60,32 +48,47 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
const { pageNotes, ...pagination } = usePagination(note, noteIds);
const [ includeArchived ] = useNoteLabelBoolean(note, "includeArchived");
const noteType = useNoteProperty(note, "type");
const hasCollectionProperties = [ "book", "search" ].includes(noteType ?? "");
return (
<div className="note-list grid-view">
<CollectionProperties
note={note}
centerChildren={<Pager {...pagination} />}
/>
<div className="note-list-wrapper">
{!hasCollectionProperties && <Pager {...pagination} />}
<div className={clsx("note-list-container use-tn-links", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<GridNoteCard key={childNote.noteId}
note={childNote}
parentNote={note}
highlightedTokens={highlightedTokens}
includeArchived={includeArchived} />
))}
</div>
<Pager {...pagination} />
</div>
return <NoteList note={note} viewMode="grid-view" noteIds={noteIds} pagination={pagination}>
<div className={clsx("note-list-container use-tn-links", {"search-results": (noteType === "search")})}>
{pageNotes?.map(childNote => (
<GridNoteCard key={childNote.noteId}
note={childNote}
parentNote={note}
highlightedTokens={highlightedTokens}
includeArchived={includeArchived} />
))}
</div>
);
</NoteList>
}
interface NoteListProps {
note: FNote,
viewMode: "list-view" | "grid-view",
noteIds: string[],
pagination: PaginationContext,
children: ComponentChildren
}
function NoteList(props: NoteListProps) {
const noteType = useNoteProperty(props.note, "type");
const hasCollectionProperties = ["book", "search"].includes(noteType ?? "");
return <div className={clsx("note-list", props.viewMode)}>
<CollectionProperties
note={props.note}
centerChildren={<Pager className="note-list-top-pager" {...props.pagination} />}
/>
{props.noteIds.length > 0 && <div className="note-list-wrapper">
{!hasCollectionProperties && <Pager {...props.pagination} />}
{props.children}
<Pager className="note-list-bottom-pager" {...props.pagination} />
</div>}
</div>
}
function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expandDepth, includeArchived }: {
@ -175,7 +178,8 @@ function GridNoteCard(props: GridNoteCardProps) {
showNotePath={props.parentNote.type === "search"}
highlightedTokens={props.highlightedTokens}
/>
<NoteMenuButton notePath={notePath} />
{!props.note.isOptions() && <NoteMenuButton notePath={notePath} />}
</h5>
<NoteContent note={props.note}
trim
@ -210,6 +214,17 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
const [ready, setReady] = useState(false);
const [noteType, setNoteType] = useState<string>("none");
useEffect(() => {
const contentElement = contentRef.current;
if (!contentElement) return;
contentSizeObserver.observe(contentElement);
return () => {
contentSizeObserver.unobserve(contentElement);
}
}, []);
useEffect(() => {
content_renderer.getRenderedContent(note, {
trim,
@ -296,4 +311,11 @@ function useExpansionDepth(note: FNote) {
}
return parseInt(expandDepth, 10);
}
function onContentResized(entries: ResizeObserverEntry[], observer: ResizeObserver): void {
for (const contentElement of entries) {
const isOverflowing = ((contentElement.target.scrollHeight > contentElement.target.clientHeight))
contentElement.target.classList.toggle("note-book-content-overflowing", isOverflowing);
}
}

View File

@ -3,7 +3,7 @@ import { LOCALES } from "@triliumnext/commons";
import { EventData } from "../../components/app_context.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
import options from "../../services/options.js";
import utils, { isMobile } from "../../services/utils.js";
import utils, { isIOS, isMobile } from "../../services/utils.js";
import { readCssVar } from "../../utils/css-var.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
@ -13,15 +13,20 @@ import FlexContainer from "./flex_container.js";
*
* For convenience, the root container has a few class selectors that can be used to target some global state:
*
* - `#root-container.light-theme`, indicates whether the current color scheme is light.
* - `#root-container.dark-theme`, indicates whether the current color scheme is dark.
* - `#root-container.virtual-keyboard-opened`, on mobile devices if the virtual keyboard is open.
* - `#root-container.horizontal-layout`, if the current layout is horizontal.
* - `#root-container.vertical-layout`, if the current layout is horizontal.
*/
export default class RootContainer extends FlexContainer<BasicWidget> {
private originalWindowHeight: number;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.originalWindowHeight = window.innerHeight ?? 0;
this.id("root-widget");
this.css("height", "100dvh");
}
@ -31,6 +36,8 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#initTheme();
this.#setDeviceSpecificClasses();
this.#setMaxContentWidth();
this.#setMotion();
this.#setShadows();
@ -63,9 +70,24 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
}
#initTheme() {
const colorSchemeChangeObserver = matchMedia("(prefers-color-scheme: dark)")
colorSchemeChangeObserver.addEventListener("change", () => this.#updateColorScheme());
this.#updateColorScheme();
document.body.setAttribute("data-theme-id", options.get("theme"));
}
#updateColorScheme() {
const colorScheme = readCssVar(document.body, "theme-style").asString();
document.body.classList.toggle("light-theme", colorScheme === "light");
document.body.classList.toggle("dark-theme", colorScheme === "dark");
}
#onMobileResize() {
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const windowHeight = window.innerHeight;
const windowHeight = Math.max(window.innerHeight, this.originalWindowHeight); // inner height changes when keyboard is opened, we need to compare with the original height to detect it.
// If viewport is significantly smaller, keyboard is likely open
const isKeyboardOpened = windowHeight - viewportHeight > 150;
@ -117,6 +139,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}
#setDeviceSpecificClasses() {
if (isIOS()) {
document.body.classList.add("ios");
}
}
#initPWATopbarColor() {
if (!utils.isPWA()) return;
const tracker = $("#background-color-tracker");

View File

@ -1,5 +1,5 @@
.collection-properties {
padding: 0.55em 12px;
padding: 0.55em var(--content-margin-inline);
display: flex;
gap: 0.25em;
align-items: center;

View File

@ -26,6 +26,7 @@ export default function NoteTitleWidget(props: {className?: string}) {
|| note === undefined
|| (note.isProtected && !protected_session_holder.isProtectedSessionAvailable())
|| isLaunchBarConfig(note.noteId)
|| note.noteId.startsWith("_help_")
|| viewScope?.viewMode !== "default";
setReadOnly(isReadOnly);
}, [ note, note?.noteId, note?.isProtected, viewScope?.viewMode ]);

View File

@ -1,11 +1,11 @@
import FlexContainer from "./containers/flex_container.js";
import utils from "../services/utils.js";
import attributeService from "../services/attributes.js";
import type BasicWidget from "./basic_widget.js";
import type { EventData } from "../components/app_context.js";
import type NoteContext from "../components/note_context.js";
import type FNote from "../entities/fnote.js";
import attributeService from "../services/attributes.js";
import { getLocaleById } from "../services/i18n.js";
import utils from "../services/utils.js";
import type BasicWidget from "./basic_widget.js";
import FlexContainer from "./containers/flex_container.js";
export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
@ -43,11 +43,16 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
refresh() {
const isHiddenExt = this.isHiddenExt(); // preserve through class reset
const isActive = this.$widget.hasClass("active");
this.$widget.removeClass();
this.toggleExt(!isHiddenExt);
if (isActive) {
this.$widget.addClass("active");
}
this.$widget.addClass("component note-split");
const note = this.noteContext?.note;
@ -92,6 +97,11 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
#hasBackgroundEffects(note: FNote): boolean {
const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [
"application/pdf"
];
const COLLECTIONS_WITH_BACKGROUND_EFFECTS = [
"grid",
"list"
]
if (note.isOptions()) {
@ -102,6 +112,10 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
return true;
}
if (note.type === "book" && COLLECTIONS_WITH_BACKGROUND_EFFECTS.includes(note.getLabelValue("viewType") ?? "none")) {
return true;
}
return false;
}

View File

@ -7,7 +7,7 @@
}
.tn-card-frame,
.tn-card-section {
.tn-card-body .tn-card-section {
padding: var(--card-padding-block) var(--card-padding-inline);
border: 1px solid var(--card-border-color, var(--main-border-color));
background: var(--card-background-color);

View File

@ -1383,3 +1383,28 @@ export function useGetContextDataFrom<K extends keyof NoteContextDataMap>(
return data;
}
export function useColorScheme() {
const themeStyle = getThemeStyle();
const defaultValue = themeStyle === "auto" ? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) : themeStyle === "dark";
const [ prefersDark, setPrefersDark ] = useState(defaultValue);
useEffect(() => {
if (themeStyle !== "auto") return;
const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)");
const listener = (e: MediaQueryListEvent) => setPrefersDark(e.matches);
mediaQueryList.addEventListener("change", listener);
return () => mediaQueryList.removeEventListener("change", listener);
}, [ themeStyle ]);
return prefersDark ? "dark" : "light";
}
function getThemeStyle() {
const style = window.getComputedStyle(document.body);
const themeStyle = style.getPropertyValue("--theme-style");
if (style.getPropertyValue("--theme-style-auto") !== "true" && (themeStyle === "light" || themeStyle === "dark")) {
return themeStyle as "light" | "dark";
}
return "auto";
}

View File

@ -8,6 +8,7 @@ export default function ScrollPadding() {
const [height, setHeight] = useState<number>(10);
const isEnabled = ["text", "code"].includes(note?.type ?? "")
&& viewScope?.viewMode === "default"
&& note?.isContentAvailable()
&& !note?.isTriliumSqlite();
const refreshHeight = () => {

View File

@ -6,12 +6,12 @@ import "./MindMap.css";
import nodeMenu from "@mind-elixir/node-menu";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { snapdom } from "@zumer/snapdom";
import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options } from "mind-elixir";
import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME, DARK_THEME } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
import utils from "../../services/utils";
import { useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { useColorScheme, useEditorSpacedUpdate, useNoteLabelBoolean, useSyncedRef, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { refToJQuerySelector } from "../react/react_utils";
import { TypeWidgetProps } from "./type_widget";
@ -85,9 +85,11 @@ export default function MindMap({ note, ntxId, noteContext }: TypeWidgetProps) {
},
onContentChange: (content) => {
let newContent: MindElixirData;
if (content) {
try {
newContent = JSON.parse(content) as MindElixirData;
delete newContent.theme; // The theme is managed internally by the widget, so we remove it from the loaded content to avoid inconsistencies.
} catch (e) {
console.warn(e);
console.debug("Wrong JSON content: ", content);
@ -151,6 +153,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
const apiRef = useRef<MindElixirInstance>(null);
const [ locale ] = useTriliumOption("locale");
const colorScheme = useColorScheme();
function reinitialize() {
if (!containerRef.current) return;
@ -158,7 +161,8 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
const mind = new VanillaMindElixir({
el: containerRef.current,
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable
editable,
theme: LIGHT_THEME
});
if (editable) {
@ -179,6 +183,14 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
};
}, []);
// React to theme changes.
useEffect(() => {
if (!apiRef.current) return;
const newTheme = colorScheme === "dark" ? DARK_THEME : LIGHT_THEME;
if (apiRef.current.theme === newTheme) return; // Avoid unnecessary theme changes, which can be expensive to render.
apiRef.current.changeTheme(newTheme);
}, [ colorScheme ]);
useEffect(() => {
const data = apiRef.current?.getData();
reinitialize();

View File

@ -1,7 +1,7 @@
import { Excalidraw } from "@excalidraw/excalidraw";
import { TypeWidgetProps } from "../type_widget";
import "@excalidraw/excalidraw/index.css";
import { useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import { useColorScheme, useNoteLabelBoolean, useTriliumOption } from "../../react/hooks";
import { useCallback, useMemo, useRef } from "preact/hooks";
import { type ExcalidrawImperativeAPI, type AppState } from "@excalidraw/excalidraw/types";
import options from "../../../services/options";
@ -19,12 +19,9 @@ window.EXCALIDRAW_ASSET_PATH = `${window.location.pathname}/node_modules/@excali
export default function Canvas({ note, noteContext }: TypeWidgetProps) {
const apiRef = useRef<ExcalidrawImperativeAPI>(null);
const [ isReadOnly ] = useNoteLabelBoolean(note, "readOnly");
const themeStyle = useMemo(() => {
const documentStyle = window.getComputedStyle(document.documentElement);
return documentStyle.getPropertyValue("--theme-style")?.trim() as AppState["theme"];
}, []);
const colorScheme = useColorScheme();
const [ locale ] = useTriliumOption("locale");
const persistence = useCanvasPersistence(note, noteContext, apiRef, themeStyle, isReadOnly);
const persistence = useCanvasPersistence(note, noteContext, apiRef, colorScheme, isReadOnly);
/** Use excalidraw's native zoom instead of the global zoom. */
const onWheel = useCallback((e: MouseEvent) => {
@ -54,7 +51,7 @@ export default function Canvas({ note, noteContext }: TypeWidgetProps) {
<div className="excalidraw-wrapper">
<Excalidraw
excalidrawAPI={api => apiRef.current = api}
theme={themeStyle}
theme={colorScheme}
viewModeEnabled={isReadOnly || options.is("databaseReadonly")}
zenModeEnabled={false}
isCollaborating={false}

View File

@ -2,22 +2,14 @@ body.mobile {
.classic-toolbar-outer-container {
contain: none !important;
}
.classic-toolbar-outer-container.visible {
height: 38px;
background-color: var(--main-background-color);
position: relative;
overflow: visible;
flex-shrink: 0;
}
#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios {
position: absolute;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
}
.classic-toolbar-widget {
position: absolute;
bottom: 0;
@ -28,27 +20,27 @@ body.mobile {
display: flex;
align-items: flex-end;
user-select: none;
touch-action: pan-x;
scrollbar-width: 0 !important;
}
.classic-toolbar-widget::-webkit-scrollbar:horizontal {
height: 0 !important;
}
.classic-toolbar-widget.dropdown-active {
height: 50vh;
}
.classic-toolbar-widget .ck.ck-toolbar {
--ck-color-toolbar-background: transparent;
--ck-color-toolbar-background: var(--main-background-color);
--ck-color-button-default-background: transparent;
--ck-color-button-default-disabled-background: transparent;
position: absolute;
background-color: transparent;
border: none;
}
.classic-toolbar-widget .ck.ck-button.ck-disabled {
opacity: 0.3;
}
}
}

View File

@ -66,11 +66,22 @@ export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolb
}
function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef<HTMLDivElement | null>) {
// Capture the baseline offset (Safari nav bar height) before the keyboard opens.
const baselineOffset = useRef(window.innerHeight - (window.visualViewport?.height ?? window.innerHeight));
const adjustPosition = useCallback(() => {
if (!wrapperRef.current) return;
const bottom = window.innerHeight - (window.visualViewport?.height || 0);
wrapperRef.current.style.bottom = `${bottom}px`;
}, []);
const viewport = window.visualViewport;
if (!viewport) return;
// Subtract the baseline so only the keyboard's contribution remains.
const bottom = window.innerHeight - viewport.height - viewport.offsetTop;
if (bottom - baselineOffset.current <= 0) {
// Keyboard is hidden — clear the inline style so CSS controls positioning.
wrapperRef.current.style.removeProperty("bottom");
} else {
wrapperRef.current.style.bottom = `${bottom}px`;
}
}, [ wrapperRef ]);
useEffect(() => {
if (!isIOS() || !enabled) return;
@ -82,7 +93,7 @@ function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef<HTMLDivEle
window.visualViewport?.removeEventListener("resize", adjustPosition);
window.removeEventListener("scroll", adjustPosition);
};
}, [ enabled ]);
}, [ enabled, adjustPosition ]);
}
/**

View File

@ -35,7 +35,7 @@
"@triliumnext/commons": "workspace:*",
"@triliumnext/server": "workspace:*",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.0",
"electron": "40.6.1",
"@electron-forge/cli": "7.11.1",
"@electron-forge/maker-deb": "7.11.1",
"@electron-forge/maker-dmg": "7.11.1",

View File

@ -12,7 +12,7 @@
"@triliumnext/desktop": "workspace:*",
"@types/fs-extra": "11.0.4",
"copy-webpack-plugin": "13.0.1",
"electron": "40.6.0",
"electron": "40.6.1",
"fs-extra": "11.3.3"
},
"scripts": {

View File

@ -1,4 +1,4 @@
FROM node:24.13.1-bullseye-slim AS builder
FROM node:24.14.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.13.1-bullseye-slim
FROM node:24.14.0-bullseye-slim
# Install only runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \

View File

@ -1,4 +1,4 @@
FROM node:24.13.1-alpine AS builder
FROM node:24.14.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.13.1-alpine
FROM node:24.14.0-alpine
# Install runtime dependencies
RUN apk add --no-cache su-exec shadow

View File

@ -1,4 +1,4 @@
FROM node:24.13.1-alpine AS builder
FROM node:24.14.0-alpine AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.13.1-alpine
FROM node:24.14.0-alpine
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@ -1,4 +1,4 @@
FROM node:24.13.1-bullseye-slim AS builder
FROM node:24.14.0-bullseye-slim AS builder
RUN corepack enable
# Install native dependencies since we might be building cross-platform.
@ -7,7 +7,7 @@ COPY ./docker/package.json ./docker/pnpm-workspace.yaml /usr/src/app/
# We have to use --no-frozen-lockfile due to CKEditor patches
RUN pnpm install --no-frozen-lockfile --prod && pnpm rebuild
FROM node:24.13.1-bullseye-slim
FROM node:24.14.0-bullseye-slim
# Create a non-root user with configurable UID/GID
ARG USER=trilium
ARG UID=1001

View File

@ -62,7 +62,7 @@
"@types/serve-favicon": "2.5.7",
"@types/serve-static": "2.2.0",
"@types/stream-throttle": "0.1.4",
"@types/supertest": "6.0.3",
"@types/supertest": "7.2.0",
"@types/tmp": "0.2.6",
"@types/turndown": "5.0.6",
"@types/ws": "8.18.1",
@ -82,7 +82,7 @@
"debounce": "3.0.0",
"debug": "4.4.3",
"ejs": "4.0.1",
"electron": "40.6.0",
"electron": "40.6.1",
"electron-debug": "4.1.0",
"electron-window-state": "5.0.3",
"escape-html": "1.0.3",

View File

@ -179,10 +179,10 @@ describe("Markdown export", () => {
> [!IMPORTANT]
> This is a very important information.
>${space}
> | | |
> | | |
> | --- | --- |
> | 1 | 2 |
> | 3 | 4 |
> | 1 | 2 |
> | 3 | 4 |
> [!CAUTION]
> This is a caution.
@ -374,10 +374,10 @@ describe("Markdown export", () => {
</figure>
`;
const expected = trimIndentation`\
| | |
| | |
| --- | --- |
| Hi | there |
| Hi | there |`;
| Hi | there |
| Hi | there |`;
expect(markdownExportService.toMarkdown(html)).toBe(expected);
});

View File

@ -13,10 +13,10 @@
"postinstall": "wxt prepare"
},
"keywords": [],
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.30.2",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.0",
"wxt": "0.20.17"
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.18"
},
"dependencies": {
"cash-dom": "8.1.5"

View File

@ -18,7 +18,7 @@
},
"devDependencies": {
"@preact/preset-vite": "2.10.3",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-preact": "2.0.0",
"typescript": "5.9.3",
"user-agent-data-types": "0.4.2",

View File

@ -50,7 +50,7 @@
"@triliumnext/server": "workspace:*",
"@types/express": "5.0.6",
"@types/js-yaml": "4.0.9",
"@types/node": "24.10.13",
"@types/node": "24.10.14",
"@vitest/browser-webdriverio": "4.0.18",
"@vitest/coverage-v8": "4.0.18",
"@vitest/ui": "4.0.18",
@ -58,10 +58,10 @@
"cross-env": "10.1.0",
"dpdm": "4.0.1",
"esbuild": "0.27.3",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-preact": "2.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-playwright": "2.7.0",
"eslint-plugin-playwright": "2.7.1",
"eslint-plugin-simple-import-sort": "12.1.1",
"happy-dom": "20.7.0",
"http-server": "14.1.1",
@ -73,7 +73,7 @@
"tslib": "2.8.1",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.56.0",
"typescript-eslint": "8.56.1",
"upath": "2.0.1",
"vite": "7.3.1",
"vite-plugin-dts": "4.5.4",
@ -93,7 +93,7 @@
"url": "https://github.com/TriliumNext/Trilium/issues"
},
"homepage": "https://triliumnotes.org",
"packageManager": "pnpm@10.30.1",
"packageManager": "pnpm@10.30.2",
"pnpm": {
"patchedDependencies": {
"@ckeditor/ckeditor5-mention": "patches/@ckeditor__ckeditor5-mention.patch",

View File

@ -24,16 +24,16 @@
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "17.3.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",

View File

@ -25,16 +25,16 @@
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "17.3.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",

View File

@ -27,16 +27,16 @@
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "17.3.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",

View File

@ -27,16 +27,16 @@
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "17.3.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",

View File

@ -27,16 +27,16 @@
"@ckeditor/ckeditor5-dev-build-tools": "54.3.3",
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
"@ckeditor/ckeditor5-package-tools": "5.0.1",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "4.0.18",
"@vitest/coverage-istanbul": "4.0.18",
"ckeditor5": "47.4.0",
"eslint": "10.0.1",
"eslint": "10.0.2",
"eslint-config-ckeditor5": ">=9.1.0",
"http-server": "14.1.1",
"lint-staged": "16.2.7",
"stylelint": "17.3.0",
"stylelint": "17.4.0",
"stylelint-config-ckeditor5": ">=9.1.0",
"ts-node": "10.9.2",
"typescript": "5.9.3",

View File

@ -16,7 +16,7 @@
"ckeditor5-premium-features": "47.4.0"
},
"devDependencies": {
"@smithy/middleware-retry": "4.4.33",
"@types/jquery": "3.5.33"
"@smithy/middleware-retry": "4.4.37",
"@types/jquery": "4.0.0"
}
}

View File

@ -50,6 +50,6 @@
"codemirror-lang-elixir": "4.0.0",
"codemirror-lang-hcl": "0.1.0",
"codemirror-lang-mermaid": "0.5.0",
"eslint-linter-browserify": "10.0.1"
"eslint-linter-browserify": "10.0.2"
}
}

View File

@ -31,11 +31,11 @@
"devDependencies": {
"@digitak/esrun": "3.2.26",
"@triliumnext/ckeditor5": "workspace:*",
"@typescript-eslint/eslint-plugin": "8.56.0",
"@typescript-eslint/parser": "8.56.0",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"dotenv": "17.3.1",
"esbuild": "0.27.3",
"eslint": "10.0.1",
"eslint": "10.0.2",
"highlight.js": "11.11.1",
"typescript": "5.9.3"
}

View File

@ -96,7 +96,8 @@ rules.table = {
var columnCount = tableColCount(node);
var emptyHeader = ''
if (columnCount && !secondLineIsDivider) {
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|'
// MD060 compact style: 2 spaces between pipes for empty cells
emptyHeader = '|' + ' |'.repeat(columnCount) + '\n' + '|'
for (var columnIndex = 0; columnIndex < columnCount; ++columnIndex) {
emptyHeader += ' ' + getBorder(getColumnAlignment(node, columnIndex)) + ' |';
}
@ -157,13 +158,15 @@ function isFirstTbody (element) {
)
}
// Format table cells following MD060 compact style:
// Each cell has 1 space padding on left and right (prefix + content + ' |').
// Empty cells result in 2 spaces between pipes (1 left + 1 right padding).
function cell (content, node = null, index = null) {
if (index === null) index = indexOf.call(node.parentNode.childNodes, node)
var prefix = ' '
if (index === 0) prefix = '| '
let filteredContent = content.trim().replace(/\n\r/g, '<br>').replace(/\n/g, "<br>");
filteredContent = filteredContent.replace(/\|+/g, '\\|')
while (filteredContent.length < 3) filteredContent += ' ';
if (node) filteredContent = handleColSpan(filteredContent, node, ' ');
return prefix + filteredContent + ' |'
}
@ -259,7 +262,7 @@ function nodeParentTable(node) {
function handleColSpan(content, node, emptyChar) {
const colspan = node.getAttribute('colspan') || 1;
for (let i = 1; i < colspan; i++) {
content += ' | ' + emptyChar.repeat(3);
content += ' |' + emptyChar;
}
return content
}

View File

@ -141,11 +141,11 @@
</div>
<pre class="expected">| Column 1 | Column 2 | Column 3 | Column 4 |
| --- | --- | --- | --- |
| | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | | Row 2, Column 3 | Row 2, Column 4 |
| Row 3, Column 1 | Row 3, Column 2 | | Row 3, Column 4 |
| Row 4, Column 1 | Row 4, Column 2 | Row 4, Column 3 | |
| | | | Row 5, Column 4 |</pre>
| | Row 1, Column 2 | Row 1, Column 3 | Row 1, Column 4 |
| Row 2, Column 1 | | Row 2, Column 3 | Row 2, Column 4 |
| Row 3, Column 1 | Row 3, Column 2 | | Row 3, Column 4 |
| Row 4, Column 1 | Row 4, Column 2 | Row 4, Column 3 | |
| | | | Row 5, Column 4 |</pre>
</div>
<div class="case" data-name="empty rows">
@ -174,7 +174,7 @@
<pre class="expected">| Heading 1 | Heading 2 |
| --- | --- |
| Row 1 | Row 1 |
| | |
| | |
| Row 3 | Row 3 |</pre>
</div>
@ -259,7 +259,7 @@
<tbody><tr><th>Heading</th></tr></tbody>
</table>
</div>
<pre class="expected">| |
<pre class="expected">| |
| --- |
| Heading |
| --- |</pre>
@ -272,7 +272,7 @@
<tr><td>Row 2 Cell 1</td><td>Row 2 Cell 2</td></tr>
</table>
</div>
<pre class="expected">| | |
<pre class="expected">| | |
| --- | --- |
| Row 1 Cell 1 | Row 1 Cell 2 |
| Row 2 Cell 1 | Row 2 Cell 2 |</pre>
@ -291,7 +291,7 @@
</tr>
</table>
</div>
<pre class="expected">| | |
<pre class="expected">| | |
| --- | --- |
| Heading | Not a heading |
| Heading | Not a heading |</pre>

1945
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff