From 634e0b6d3034d97fdd318c3f25e726fd8928a5f9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 3 Feb 2026 18:04:08 +0200 Subject: [PATCH 01/36] feat(mobile/note_actions): integrate note info --- apps/client/src/widgets/layout/StatusBar.tsx | 16 +++++----- .../mobile_widgets/mobile_detail_menu.tsx | 31 +++++++++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index bfc9b02648..c2b8b5f697 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) { //#region Note info & Similar interface NoteInfoContext extends StatusBarContext { - similarNotesShown: boolean; - setSimilarNotesShown: (value: boolean) => void; + similarNotesShown?: boolean; + setSimilarNotesShown?: (value: boolean) => void; } export function NoteInfoBadge(context: NoteInfoContext) { @@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) { // Keyboard shortcut. useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show()); - useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown)); + useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown)); return (enabled && ; +export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick & { + dropdownRef?: RefObject; noteType: NoteType; }) { const { metadata, ...sizeProps } = useNoteMetadata(note); @@ -262,13 +262,13 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: } /> - { - dropdownRef.current?.hide(); + dropdownRef?.current?.hide(); setSimilarNotesShown(true); }} - /> + />} ); } diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index dc0c5e89cd..769ac20ab4 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -4,6 +4,7 @@ import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; import note_create from "../../services/note_create"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; +import { NoteInfoContent } from "../layout/StatusBar"; import ActionButton from "../react/ActionButton"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useNoteContext } from "../react/hooks"; @@ -18,6 +19,7 @@ export default function MobileDetailMenu() { const isMainContext = noteContext?.isMainContext(); const [ backlinksModalShown, setBacklinksModalShown ] = useState(false); const [ notePathsModalShown, setNotePathsModalShown ] = useState(false); + const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default"); @@ -72,6 +74,11 @@ export default function MobileDetailMenu() { >{t("close_pane_button.close_this_pane")} } + setNoteInfoModalShown(true)} + >{t("note_info_widget.title")} + } /> ) : ( @@ -86,13 +93,19 @@ export default function MobileDetailMenu() { <> + ), document.body)} ); } -function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) { +interface WithModal { + modalShown: boolean; + setModalShown: (shown: boolean) => void; +} + +function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { return ( void }) { +function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) { return ( ); } + +function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + return ( + setModalShown(false)} + > + {note && } + + ); +} From c7265017b3314d534ff58fcea742f67e4d4dbcb1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 3 Feb 2026 18:09:02 +0200 Subject: [PATCH 02/36] feat(mobile/note_actions): integrate styling for note info --- apps/client/src/widgets/layout/StatusBar.css | 56 ++++++++++---------- apps/client/src/widgets/layout/StatusBar.tsx | 4 +- 2 files changed, 31 insertions(+), 29 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 6418630dba..c1dad02671 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -58,33 +58,6 @@ .dropdown-note-info { padding: 1em !important; - - ul { - --row-block-margin: .2em; - - list-style-type: none; - padding: 0; - margin: 0; - margin-top: calc(0px - var(--row-block-margin)); - margin-bottom: 12px; - display: table; - - li { - display: table-row; - - > strong { - display: table-cell; - padding: var(--row-block-margin) 0; - opacity: .5; - } - - > span { - display: table-cell; - user-select: text; - padding-left: 2em; - } - } - } } .dropdown-note-paths { @@ -154,6 +127,35 @@ } +body.experimental-feature-new-layout .note-info-content { + ul { + --row-block-margin: .2em; + + list-style-type: none; + padding: 0; + margin: 0; + margin-top: calc(0px - var(--row-block-margin)); + margin-bottom: 12px; + display: table; + + li { + display: table-row; + + > strong { + display: table-cell; + padding: var(--row-block-margin) 0; + opacity: .5; + } + + > span { + display: table-cell; + user-select: text; + padding-left: 2em; + } + } + } +} + .bottom-panel { margin: 0 !important; padding: 0; diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index c2b8b5f697..c608c8f203 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -251,7 +251,7 @@ export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdown const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]); return ( - <> +
    {originalFileName && } @@ -269,7 +269,7 @@ export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdown setSimilarNotesShown(true); }} />} - +
); } From 52b41b1bb0e74a2f2e65e05cee934dc630224c88 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 3 Feb 2026 18:18:57 +0200 Subject: [PATCH 03/36] feat(mobile/note_actions): integrate similar notes --- apps/client/src/widgets/layout/StatusBar.css | 13 +++++++---- .../mobile_widgets/mobile_detail_menu.tsx | 23 +++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index c1dad02671..a0b22c8d26 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -117,10 +117,6 @@ } - div.similar-notes-widget div.similar-notes-wrapper { - max-height: unset; - } - button.select-button:not(:focus-visible) { outline: none; } @@ -156,6 +152,15 @@ body.experimental-feature-new-layout .note-info-content { } } +body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper { + max-height: unset; +} + +body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper { + max-height: unset; + padding: 0; +} + .bottom-panel { margin: 0 !important; padding: 0; diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 769ac20ab4..3a89bdec85 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -12,6 +12,7 @@ import Modal from "../react/Modal"; import { NoteContextMenu } from "../ribbon/NoteActions"; import NoteActionsCustom from "../ribbon/NoteActionsCustom"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; +import SimilarNotesTab from "../ribbon/SimilarNotesTab"; export default function MobileDetailMenu() { const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext(); @@ -20,6 +21,7 @@ export default function MobileDetailMenu() { const [ backlinksModalShown, setBacklinksModalShown ] = useState(false); const [ notePathsModalShown, setNotePathsModalShown ] = useState(false); const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false); + const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default"); @@ -74,10 +76,8 @@ export default function MobileDetailMenu() { >{t("close_pane_button.close_this_pane")} } - setNoteInfoModalShown(true)} - >{t("note_info_widget.title")} + setNoteInfoModalShown(true)} >{t("note_info_widget.title")} + setSimilarNotesModalShown(true)}>{t("similar_notes.title")} } /> @@ -94,6 +94,7 @@ export default function MobileDetailMenu() { + ), document.body)} @@ -153,3 +154,17 @@ function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null
); } + +function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + return ( + setModalShown(false)} + > + + + ); +} From 0d8453f6a76009251476df79d2e5d8f7744b1b5a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 3 Feb 2026 18:58:04 +0200 Subject: [PATCH 04/36] feat(mobile/note_actions): integrate code language switcher --- apps/client/src/stylesheets/style.css | 1 + .../mobile_widgets/mobile_detail_menu.css | 3 ++ .../mobile_widgets/mobile_detail_menu.tsx | 38 ++++++++++++++++- .../src/widgets/ribbon/BasicPropertiesTab.tsx | 42 +++++++++---------- 4 files changed, 61 insertions(+), 23 deletions(-) create mode 100644 apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index e3d3f5279f..cac9bc2989 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -409,6 +409,7 @@ body.desktop .tabulator-popup-container, .dropdown-menu.static { box-shadow: unset; + backdrop-filter: unset !important; } .dropend .dropdown-toggle::after { diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css new file mode 100644 index 0000000000..f3e1503471 --- /dev/null +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.css @@ -0,0 +1,3 @@ +.code-note-switcher-modal .dropdown-menu { + background: none !important; +} diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 3a89bdec85..53bb58c50e 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -1,14 +1,18 @@ +import "./mobile_detail_menu.css"; + import { createPortal, useState } from "preact/compat"; import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; import note_create from "../../services/note_create"; +import server from "../../services/server"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import { NoteInfoContent } from "../layout/StatusBar"; import ActionButton from "../react/ActionButton"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useNoteContext } from "../react/hooks"; +import { useNoteContext, useNoteProperty } from "../react/hooks"; import Modal from "../react/Modal"; +import { NoteTypeCodeNoteList, useMimeTypes } from "../ribbon/BasicPropertiesTab"; import { NoteContextMenu } from "../ribbon/NoteActions"; import NoteActionsCustom from "../ribbon/NoteActionsCustom"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; @@ -22,6 +26,7 @@ export default function MobileDetailMenu() { const [ notePathsModalShown, setNotePathsModalShown ] = useState(false); const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false); const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false); + const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default"); @@ -76,7 +81,8 @@ export default function MobileDetailMenu() { >{t("close_pane_button.close_this_pane")} } - setNoteInfoModalShown(true)} >{t("note_info_widget.title")} + {note.type === "code" && setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}} + setNoteInfoModalShown(true)}>{t("note_info_widget.title")} setSimilarNotesModalShown(true)}>{t("similar_notes.title")} } @@ -95,6 +101,7 @@ export default function MobileDetailMenu() { + ), document.body)} @@ -168,3 +175,30 @@ function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote |
); } + +function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) { + const currentNoteMime = useNoteProperty(note, "mime"); + const mimeTypes = useMimeTypes(); + + return ( + setModalShown(false)} + > +
+ {note && { + server.put(`notes/${note.noteId}/type`, { type, mime }); + setModalShown(false); + }} + // setModalShown={() => setModalShown(true)} + />} +
+
+ ); +} diff --git a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx index 9b800bebb9..5dbd0d29cb 100644 --- a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx @@ -1,5 +1,4 @@ import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons"; -import { ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks"; @@ -117,19 +116,18 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note onClick={() => changeNoteType(type, mime)} >{title} ); - } else { - return ( - <> - - - {title} - - - ); } + return ( + <> + + + {title} + + + ); })} {!noCodeNotes && } @@ -141,7 +139,7 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp currentMimeType?: string; mimeTypes: MimeType[]; changeNoteType(type: NoteType, mime: string): void; - setModalShown(shown: boolean): void; + setModalShown?(shown: boolean): void; }) { return ( <> @@ -155,8 +153,10 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp ))} - - setModalShown(true)}>{t("basic_properties.configure_code_notes")} + {setModalShown && <> + + setModalShown(true)}>{t("basic_properties.configure_code_notes")} + } ); } @@ -195,7 +195,7 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) { onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)} /> - ) + ); } function EditabilitySelect({ note }: { note?: FNote | null }) { @@ -417,9 +417,9 @@ function findTypeTitle(type?: NoteType, mime?: string | null) { const found = mimeTypes.find((mt) => mt.mime === mime); return found ? found.title : mime; - } else { - const noteType = NOTE_TYPES.find((nt) => nt.type === type); - - return noteType ? noteType.title : type; } + const noteType = NOTE_TYPES.find((nt) => nt.type === type); + + return noteType ? noteType.title : type; + } From 2cbe96d8158d8977b44a794e73bd6cee1012a184 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 18:03:17 +0200 Subject: [PATCH 05/36] feat(mobile/note_actions): integrate text content language switcher --- .../mobile_widgets/mobile_detail_menu.tsx | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 53bb58c50e..a60267001b 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -7,16 +7,17 @@ import { t } from "../../services/i18n"; import note_create from "../../services/note_create"; import server from "../../services/server"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; -import { NoteInfoContent } from "../layout/StatusBar"; +import { getLocaleName, NoteInfoContent } from "../layout/StatusBar"; import ActionButton from "../react/ActionButton"; -import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; import { useNoteContext, useNoteProperty } from "../react/hooks"; import Modal from "../react/Modal"; -import { NoteTypeCodeNoteList, useMimeTypes } from "../ribbon/BasicPropertiesTab"; +import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab"; import { NoteContextMenu } from "../ribbon/NoteActions"; import NoteActionsCustom from "../ribbon/NoteActionsCustom"; import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; import SimilarNotesTab from "../ribbon/SimilarNotesTab"; +import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; export default function MobileDetailMenu() { const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext(); @@ -81,6 +82,7 @@ export default function MobileDetailMenu() { >{t("close_pane_button.close_this_pane")} } + {note.type === "text" && } {note.type === "code" && setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}} setNoteInfoModalShown(true)}>{t("note_info_widget.title")} setSimilarNotesModalShown(true)}>{t("similar_notes.title")} @@ -108,6 +110,31 @@ export default function MobileDetailMenu() { ); } +function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) { + const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); + const { processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id); + + return ( + + {processedLocales.map((locale, index) => + (typeof locale === "object") ? ( + setCurrentNoteLanguage(locale.id)} + >{locale.name} + ) : ( + + ) + )} + + ); +} + interface WithModal { modalShown: boolean; setModalShown: (shown: boolean) => void; From e00e3999c563f27fdc779787dbef6ecfd4c9fb24 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 18:05:37 +0200 Subject: [PATCH 06/36] feat(mobile/note_actions): indicate current content language --- apps/client/src/translations/en/translation.json | 3 ++- apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 3c2c5f6c0f..c5417f55e9 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -761,7 +761,8 @@ "note_revisions": "Note revisions", "error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'", "error_unrecognized_command": "Unrecognized command {{command}}", - "backlinks": "Backlinks" + "backlinks": "Backlinks", + "content_language_switcher": "Content language: {{language}}" }, "note_icon": { "change_note_icon": "Change note icon", diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index a60267001b..06c6f8e11c 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -112,12 +112,12 @@ export default function MobileDetailMenu() { function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) { const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); - const { processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id); + const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id); return ( {processedLocales.map((locale, index) => (typeof locale === "object") ? ( From 3e54d0ceae6bd8fe26a56d9646c62d94e6331d4f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 18:48:37 +0200 Subject: [PATCH 07/36] chore(hidden_tree): add icon to mobile tab switcher launcher --- apps/server/src/services/hidden_subtree_launcherbar.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/services/hidden_subtree_launcherbar.ts b/apps/server/src/services/hidden_subtree_launcherbar.ts index 55a3b6f70f..d93a93ca97 100644 --- a/apps/server/src/services/hidden_subtree_launcherbar.ts +++ b/apps/server/src/services/hidden_subtree_launcherbar.ts @@ -210,7 +210,8 @@ export default function buildLaunchBarConfig() { id: "_lbMobileTabSwitcher", title: t("hidden-subtree.tab-switcher-title"), type: "launcher", - builtinWidget: "mobileTabSwitcher" + builtinWidget: "mobileTabSwitcher", + icon: "bx bx-rectangle" } ]; From 8aa4a974808b88caa6955c5bfcbb60738ed5c1c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 18:52:46 +0200 Subject: [PATCH 08/36] fix(mobile): fixed tree for launcher duplicated in split --- apps/client/src/widgets/NoteDetail.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/NoteDetail.tsx b/apps/client/src/widgets/NoteDetail.tsx index 4c3e778f1b..78450811da 100644 --- a/apps/client/src/widgets/NoteDetail.tsx +++ b/apps/client/src/widgets/NoteDetail.tsx @@ -38,7 +38,7 @@ export default function NoteDetail() { const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({}); const [ activeNoteType, setActiveNoteType ] = useState(); const widgetRequestId = useRef(0); - const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile(); + const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile"); const props: TypeWidgetProps = { note: note!, @@ -216,7 +216,7 @@ export default function NoteDetail() { "fixed-tree": hasFixedTree })} > - {hasFixedTree && } + {hasFixedTree && } {Object.entries(noteTypesToRender).map(([ itemType, Element ]) => { return new NoteTreeWidget(), { noteContext }); return
{treeEl}
; } From b097f9dc21c8ece1bcef912b6ac4ada2f71e052b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 18:53:32 +0200 Subject: [PATCH 09/36] fix(mobile): fixed tree overflowing container --- apps/client/src/widgets/NoteDetail.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/NoteDetail.css b/apps/client/src/widgets/NoteDetail.css index 0f6cdbfb03..b07c6aa061 100644 --- a/apps/client/src/widgets/NoteDetail.css +++ b/apps/client/src/widgets/NoteDetail.css @@ -12,6 +12,7 @@ .fixed-note-tree-container { height: 60%; border-bottom: 1px solid var(--main-border-color); + overflow: auto; .tree-wrapper { padding: 0; From 0dc2d07b5894d1c18b0c243f29abf261a6db65e6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 19:09:18 +0200 Subject: [PATCH 10/36] fix(mobile): virtual keyboard detection not working on iOS --- .../client/src/widgets/containers/root_container.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index e81d867ef4..e460078a98 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -18,14 +18,12 @@ import FlexContainer from "./flex_container.js"; * - `#root-container.vertical-layout`, if the current layout is horizontal. */ export default class RootContainer extends FlexContainer { - private originalViewportHeight: number; constructor(isHorizontalLayout: boolean) { super(isHorizontalLayout ? "column" : "row"); this.id("root-widget"); this.css("height", "100dvh"); - this.originalViewportHeight = getViewportHeight(); } render(): JQuery { @@ -65,8 +63,12 @@ export default class RootContainer extends FlexContainer { } #onMobileResize() { - const currentViewportHeight = getViewportHeight(); - const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight); + const viewportHeight = window.visualViewport?.height ?? window.innerHeight; + const windowHeight = window.innerHeight; + + // If viewport is significantly smaller, keyboard is likely open + const isKeyboardOpened = windowHeight - viewportHeight > 150; + this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened); } @@ -115,6 +117,3 @@ export default class RootContainer extends FlexContainer { } } -function getViewportHeight() { - return window.visualViewport?.height ?? window.innerHeight; -} From ac4be3f8a8835f31bc30bf6862360d513461c59d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 20:40:31 +0200 Subject: [PATCH 11/36] feat(mobile/global_menu): add option to search notes --- apps/client/src/translations/en/translation.json | 3 ++- apps/client/src/widgets/buttons/global_menu.tsx | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index c5417f55e9..32f86b3694 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -662,7 +662,8 @@ "show-cheatsheet": "Show Cheatsheet", "toggle-zen-mode": "Zen Mode", "new-version-available": "New Update Available", - "download-update": "Get Version {{latestVersion}}" + "download-update": "Get Version {{latestVersion}}", + "search_notes": "Search notes" }, "zen_mode": { "button_exit": "Exit Zen Mode" diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 0986c8ad29..9db0393692 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -45,6 +45,10 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: noDropdownListStyle mobileBackdrop > + {isMobile() && <> + + + } @@ -105,7 +109,7 @@ function BrowserOnlyOptions() { function DevelopmentOptions({ dropStart }: { dropStart: boolean }) { return <> - + {experimentalFeatures.map((feature) => ( From b539862eefcc778b3287a2c43452ff5d87e5bdc6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 20:41:36 +0200 Subject: [PATCH 12/36] fix(mobile/search): duplicate search parameters --- apps/client/src/layouts/mobile_layout.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 0f36f282c4..8b162bced3 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -77,7 +77,6 @@ export default class MobileLayout { .child() .child() .child() - .child() .child() .child() ) From 07ce63de6965d4639fb4097337df91cebe22de94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 20:44:15 +0200 Subject: [PATCH 13/36] chore(mobile/search): remove redundant margin --- apps/client/src/widgets/layout/NoteTitleActions.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index b1aa37d89c..4ae4c15816 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -50,6 +50,9 @@ body.experimental-feature-new-layout { } } + } + + body.desktop .title-actions { > .collapsible, > .note-type-switcher { padding-inline-start: calc(24px - var(--title-actions-padding-start)); @@ -57,3 +60,4 @@ body.experimental-feature-new-layout { } } } + From 2a4d5ec1ecfe3628e2077a5af95cee108a2de480 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Wed, 4 Feb 2026 20:55:55 +0200 Subject: [PATCH 14/36] feat(mobile/search): group search options in dropdown --- .../src/translations/en/translation.json | 1 + .../src/widgets/react/ResponseContainer.tsx | 12 +++++++ .../widgets/ribbon/SearchDefinitionTab.tsx | 36 +++++++++++++------ 3 files changed, 39 insertions(+), 10 deletions(-) create mode 100644 apps/client/src/widgets/react/ResponseContainer.tsx diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 32f86b3694..7ffe41d164 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -908,6 +908,7 @@ "debug": "debug", "debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries", "action": "action", + "option": "option", "search_button": "Search", "search_execute": "Search & Execute actions", "save_to_note": "Save to note", diff --git a/apps/client/src/widgets/react/ResponseContainer.tsx b/apps/client/src/widgets/react/ResponseContainer.tsx new file mode 100644 index 0000000000..6c077a5c06 --- /dev/null +++ b/apps/client/src/widgets/react/ResponseContainer.tsx @@ -0,0 +1,12 @@ +import { ComponentChildren } from "preact"; + +import { isMobile } from "../../services/utils"; + +interface ResponsiveContainerProps { + mobile?: ComponentChildren; + desktop?: ComponentChildren; +} + +export default function ResponsiveContainer({ mobile, desktop }: ResponsiveContainerProps) { + return (isMobile() ? mobile : desktop); +} diff --git a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx index 9d182a138d..c3a54566a7 100644 --- a/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx +++ b/apps/client/src/widgets/ribbon/SearchDefinitionTab.tsx @@ -12,7 +12,7 @@ import { t } from "../../services/i18n"; import server from "../../services/server"; import toast from "../../services/toast"; import tree from "../../services/tree"; -import { getErrorMessage } from "../../services/utils"; +import { getErrorMessage, isMobile } from "../../services/utils"; import ws from "../../services/ws"; import RenameNoteBulkAction from "../bulk_actions/note/rename_note"; import Button from "../react/Button"; @@ -21,6 +21,7 @@ import { FormListHeader, FormListItem } from "../react/FormList"; import { useTriliumEvent } from "../react/hooks"; import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; +import ResponsiveContainer from "../react/ResponseContainer"; import { TabContext } from "./ribbon-interface"; import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions"; @@ -84,15 +85,30 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick {t("search_definition.add_search_option")} - {searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => ( -