Compare commits

...

32 Commits

Author SHA1 Message Date
Elian Doran
6a3c4fec98 Mobile fixes (#8832) 2026-02-25 23:17:50 +02:00
Elian Doran
6bda7837d6 chore(deps): update dependency @redocly/cli to v2.19.2 (#8826) 2026-02-25 23:07:13 +02:00
Elian Doran
ab9144972b Merge branch 'main' into feature/mobile_fixes 2026-02-25 23:06:23 +02:00
Elian Doran
bcb646f42b chore(deps): update dependency @smithy/middleware-retry to v4.4.37 (#8827) 2026-02-25 23:02:37 +02:00
Elian Doran
0db75a6cdb chore(deps): update dependency electron to v40.6.1 (#8828) 2026-02-25 23:01:37 +02:00
Elian Doran
76c7b8692d fix(deps): update dependency mind-elixir to v5.9.1 (#8829) 2026-02-25 23:00:36 +02:00
Elian Doran
1061aba212 chore(deps): update node.js to v24.14.0 (#8830) 2026-02-25 22:55:03 +02:00
Elian Doran
d4eb6f07bd chore(deps): update dependency @wxt-dev/auto-icons to v1.1.1 (#8800) 2026-02-25 22:54:42 +02:00
Elian Doran
f1a83124a8 Merge branch 'main' into feature/mobile_fixes 2026-02-25 22:42:41 +02:00
Elian Doran
2ad30c6a3d Other bugfixes (#8824) 2026-02-25 22:35:29 +02:00
Elian Doran
0f1533d0a0 chore(mobile): remove redundant style 2026-02-25 20:57:20 +02:00
Elian Doran
bf5caaebb5 fix(mobile/text): formatting toolbar doesn't go back to the right position on iOS 2026-02-25 20:36:37 +02:00
Elian Doran
2d989dcfe3 fix(mobile/text): formatting toolbar wrongly positioned if dragging on it 2026-02-25 20:18:04 +02:00
Elian Doran
8e8e6f9ed1 fix(mobile/text): floating toolbar mispositioned on iOS 2026-02-25 20:15:07 +02:00
Elian Doran
e1de98c4ae fix(mobile/text): formatting toolbar missing background on iOS 2026-02-25 20:02:15 +02:00
Elian Doran
fc59ee6e93 chore(mobile): fix warnings in mobile_editor_toolbar 2026-02-25 19:14:32 +02:00
Elian Doran
5ec9770b07 fix(mobile): confusing shift when opening keyboard in split on iOS 2026-02-25 19:08:20 +02:00
Elian Doran
1c1895b2eb fix(mobile): confusing shift when opening keyboard in split on iOS 2026-02-25 19:05:01 +02:00
Elian Doran
416825c9c2 fix(mobile): toast not respecting safe area 2026-02-25 18:54:42 +02:00
Elian Doran
6762539b4d fix(client): note context active indicator disappears after typing 2026-02-25 18:47:00 +02:00
Elian Doran
ae9827c436 fix(mobile): virtual keyboard detection not working on Android 2026-02-25 18:35:01 +02:00
Elian Doran
33622cd3fe fix(hooks): unnecessary recreation of media listener 2026-02-25 18:21:53 +02:00
renovate[bot]
e69b85a988 chore(deps): update node.js to v24.14.0 2026-02-25 00:56:05 +00:00
renovate[bot]
aabc9ec4df fix(deps): update dependency mind-elixir to v5.9.1 2026-02-25 00:55:58 +00:00
renovate[bot]
598501e3f6 chore(deps): update dependency electron to v40.6.1 2026-02-25 00:55:05 +00:00
renovate[bot]
bfa344a839 chore(deps): update dependency @smithy/middleware-retry to v4.4.37 2026-02-25 00:54:08 +00:00
renovate[bot]
c466d7ee9f chore(deps): update dependency @redocly/cli to v2.19.2 2026-02-25 00:53:13 +00:00
renovate[bot]
2e9c07d3d6 chore(deps): update dependency @wxt-dev/auto-icons to v1.1.1 2026-02-24 20:24:58 +00:00
Elian Doran
3a15878629 fix(mindmap): not reacting to switch between light/dark mode 2026-02-24 18:46:46 +02:00
Elian Doran
021a908c9c fix(canvas): not reacting to switch between light/dark mode 2026-02-24 18:35:36 +02:00
Elian Doran
c09ef3af80 fix(note_title): help pages have editable title 2026-02-24 18:14:13 +02:00
Elian Doran
3151e6dafc fix(client/layout): scroll padding enabled when note is protected 2026-02-24 18:10:51 +02:00
25 changed files with 520 additions and 442 deletions

2
.nvmrc
View File

@@ -1 +1 @@
24.13.1
24.14.0

View File

@@ -16,7 +16,7 @@
"license": "AGPL-3.0-only",
"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",

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

@@ -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

@@ -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";
@@ -19,9 +19,12 @@ import FlexContainer from "./flex_container.js";
*/
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 +34,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
}
this.#setDeviceSpecificClasses();
this.#setMaxContentWidth();
this.#setMotion();
this.#setShadows();
@@ -65,7 +69,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
#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 +121,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

@@ -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,7 +97,7 @@ 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",

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

@@ -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

@@ -15,7 +15,7 @@
"keywords": [],
"packageManager": "pnpm@10.30.2",
"devDependencies": {
"@wxt-dev/auto-icons": "1.1.0",
"@wxt-dev/auto-icons": "1.1.1",
"wxt": "0.20.18"
},
"dependencies": {

View File

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

795
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff