From ae9827c436730a0594f6100c806b75f06ab9d23e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 18:35:01 +0200 Subject: [PATCH 01/11] fix(mobile): virtual keyboard detection not working on Android --- apps/client/src/widgets/NoteDetail.tsx | 1 - apps/client/src/widgets/containers/root_container.ts | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 1b2cc8f8cc..206b2a325a 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -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"; diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index ea79893d19..d1d67e1c92 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -19,9 +19,12 @@ import FlexContainer from "./flex_container.js"; */ export default class RootContainer extends FlexContainer { + private originalWindowHeight: number; + constructor(isHorizontalLayout: boolean) { super(isHorizontalLayout ? "column" : "row"); + this.originalWindowHeight = window.innerHeight ?? 0; this.id("root-widget"); this.css("height", "100dvh"); } @@ -65,7 +68,7 @@ export default class RootContainer extends FlexContainer { #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; From 6762539b4d53a365f21020224457a16d792ebc90 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 18:47:00 +0200 Subject: [PATCH 02/11] fix(client): note context active indicator disappears after typing --- apps/client/src/widgets/note_wrapper.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/note_wrapper.ts b/apps/client/src/widgets/note_wrapper.ts index fa912e25a8..460034591d 100644 --- a/apps/client/src/widgets/note_wrapper.ts +++ b/apps/client/src/widgets/note_wrapper.ts @@ -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 { @@ -43,11 +43,16 @@ export default class NoteWrapperWidget extends FlexContainer { 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 { #hasBackgroundEffects(note: FNote): boolean { const MIME_TYPES_WITH_BACKGROUND_EFFECTS = [ "application/pdf" - ] + ]; if (note.isOptions()) { return true; From 416825c9c241d211dbd181a3f3b5efbd2a3c8306 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 18:54:42 +0200 Subject: [PATCH 03/11] fix(mobile): toast not respecting safe area --- apps/client/src/widgets/Toast.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/Toast.css b/apps/client/src/widgets/Toast.css index ecc20c2056..b58e6a13ab 100644 --- a/apps/client/src/widgets/Toast.css +++ b/apps/client/src/widgets/Toast.css @@ -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; } From 1c1895b2eb088be4209919fbc5bfc028caf8bfde Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 19:05:01 +0200 Subject: [PATCH 04/11] fix(mobile): confusing shift when opening keyboard in split on iOS --- apps/client/src/stylesheets/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 8950f3fd3f..4171a46c16 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -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; } From 5ec9770b070489d92465bdb8c29f0e09f5df1e2e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 19:08:20 +0200 Subject: [PATCH 05/11] fix(mobile): confusing shift when opening keyboard in split on iOS --- .../src/widgets/containers/root_container.ts | 9 ++++++++- .../text/mobile_editor_toolbar.css | 18 +++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index d1d67e1c92..2e852033c4 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -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"; @@ -34,6 +34,7 @@ export default class RootContainer extends FlexContainer { window.visualViewport?.addEventListener("resize", () => this.#onMobileResize()); } + this.#setDeviceSpecificClasses(); this.#setMaxContentWidth(); this.#setMotion(); this.#setShadows(); @@ -120,6 +121,12 @@ export default class RootContainer extends FlexContainer { 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"); diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css index 2286cc013d..a8fe7f3683 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css @@ -2,7 +2,7 @@ body.mobile { .classic-toolbar-outer-container { contain: none !important; } - + .classic-toolbar-outer-container.visible { height: 38px; background-color: var(--main-background-color); @@ -10,14 +10,14 @@ body.mobile { overflow: visible; flex-shrink: 0; } - - #root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios { + + #root-widget.ios.virtual-keyboard-opened .classic-toolbar-outer-container { position: absolute; inset-inline-start: 0; inset-inline-end: 0; bottom: 0; } - + .classic-toolbar-widget { position: absolute; bottom: 0; @@ -30,15 +30,15 @@ body.mobile { user-select: none; 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-button-default-background: transparent; @@ -47,8 +47,8 @@ body.mobile { background-color: transparent; border: none; } - + .classic-toolbar-widget .ck.ck-button.ck-disabled { opacity: 0.3; } -} \ No newline at end of file +} From fc59ee6e93d3f20ffa4016bd0583eef9432716e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 19:14:32 +0200 Subject: [PATCH 06/11] chore(mobile): fix warnings in mobile_editor_toolbar --- .../src/widgets/type_widgets/text/mobile_editor_toolbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx index 2d40315525..063f500574 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx @@ -70,7 +70,7 @@ function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef { if (!isIOS() || !enabled) return; @@ -82,7 +82,7 @@ function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef Date: Wed, 25 Feb 2026 20:02:15 +0200 Subject: [PATCH 07/11] fix(mobile/text): formatting toolbar missing background on iOS --- .../src/widgets/type_widgets/text/mobile_editor_toolbar.css | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css index a8fe7f3683..5f2f3c0b78 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css @@ -5,7 +5,6 @@ body.mobile { .classic-toolbar-outer-container.visible { height: 38px; - background-color: var(--main-background-color); position: relative; overflow: visible; flex-shrink: 0; @@ -40,11 +39,10 @@ body.mobile { } .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; } From 8e8e6f9ed100701ca9e7812c679a605c7c2ce757 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 20:15:07 +0200 Subject: [PATCH 08/11] fix(mobile/text): floating toolbar mispositioned on iOS --- .../widgets/type_widgets/text/mobile_editor_toolbar.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx index 063f500574..1ab9172735 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx @@ -68,8 +68,12 @@ export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolb function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef) { 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; + // Account for both viewport height and its offset within the layout viewport, + // which includes the Safari dynamic address bar height and any page scroll. + const bottom = window.innerHeight - viewport.height - viewport.offsetTop; + wrapperRef.current.style.bottom = `${Math.max(0, bottom)}px`; }, [ wrapperRef ]); useEffect(() => { From 2d989dcfe3f7d6e7a356b843e178fa1f5bea3b9b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 20:18:04 +0200 Subject: [PATCH 09/11] fix(mobile/text): formatting toolbar wrongly positioned if dragging on it --- .../src/widgets/type_widgets/text/mobile_editor_toolbar.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css index 5f2f3c0b78..9fd90e4327 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css @@ -27,6 +27,7 @@ body.mobile { display: flex; align-items: flex-end; user-select: none; + touch-action: pan-x; scrollbar-width: 0 !important; } From bf5caaebb54aa649bdb60041fcb39ff057e1d153 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 20:36:37 +0200 Subject: [PATCH 10/11] fix(mobile/text): formatting toolbar doesn't go back to the right position on iOS --- .../type_widgets/text/mobile_editor_toolbar.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx index 1ab9172735..c5e5882746 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.tsx @@ -66,14 +66,21 @@ export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolb } function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef) { + // 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 viewport = window.visualViewport; if (!viewport) return; - // Account for both viewport height and its offset within the layout viewport, - // which includes the Safari dynamic address bar height and any page scroll. + // Subtract the baseline so only the keyboard's contribution remains. const bottom = window.innerHeight - viewport.height - viewport.offsetTop; - wrapperRef.current.style.bottom = `${Math.max(0, bottom)}px`; + 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(() => { From 0f1533d0a0f03c8b8dbcc733da183b55332156da Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 25 Feb 2026 20:57:20 +0200 Subject: [PATCH 11/11] chore(mobile): remove redundant style --- .../widgets/type_widgets/text/mobile_editor_toolbar.css | 7 ------- 1 file changed, 7 deletions(-) diff --git a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css index 9fd90e4327..1ccc139f87 100644 --- a/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css +++ b/apps/client/src/widgets/type_widgets/text/mobile_editor_toolbar.css @@ -10,13 +10,6 @@ body.mobile { flex-shrink: 0; } - #root-widget.ios.virtual-keyboard-opened .classic-toolbar-outer-container { - position: absolute; - inset-inline-start: 0; - inset-inline-end: 0; - bottom: 0; - } - .classic-toolbar-widget { position: absolute; bottom: 0;