From a45fb975c09e6b71c413b5810d0004dd75d46571 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 15:53:19 +0200 Subject: [PATCH 01/38] chore(mobile/tab_switcher): add button in launch bar --- apps/client/src/layouts/mobile_layout.tsx | 2 ++ .../src/widgets/mobile_widgets/TabSwitcher.tsx | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index da66ffa13..5c7270bfd 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -13,6 +13,7 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js"; import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; +import TabSwitcher from "../widgets/mobile_widgets/TabSwitcher.jsx"; import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx"; import NoteTitleWidget from "../widgets/note_title.js"; import NoteTreeWidget from "../widgets/note_tree.js"; @@ -184,6 +185,7 @@ export default class MobileLayout { .class("horizontal") .css("height", "53px") .child() + .child() .child() .id("launcher-pane")) ) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx new file mode 100644 index 000000000..50d74c5ad --- /dev/null +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -0,0 +1,15 @@ +import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; + +export default function TabSwitcher() { + return ( + <> + { + + }} + /> + + ); +} From 3a9b448a833ed958a14c0789e3798fb0bb9fe90b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 15:59:38 +0200 Subject: [PATCH 02/38] chore(mobile/tab_switcher): create empty modal --- .../widgets/mobile_widgets/TabSwitcher.css | 6 ++++ .../widgets/mobile_widgets/TabSwitcher.tsx | 31 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 apps/client/src/widgets/mobile_widgets/TabSwitcher.css diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css new file mode 100644 index 000000000..d9e49f89e --- /dev/null +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -0,0 +1,6 @@ +.modal.tab-bar-modal { + .modal-dialog { + max-height: unset; + height: 95vh; + } +} diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 50d74c5ad..735dbfc98 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -1,15 +1,40 @@ +import "./TabSwitcher.css"; + +import { createPortal } from "preact/compat"; +import { useState } from "preact/hooks"; + import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; +import Modal from "../react/Modal"; export default function TabSwitcher() { + const [ shown, setShown ] = useState(false); + return ( <> { - - }} + onClick={() => setShown(true)} /> + {createPortal(, document.body)} ); } + + +function TabBarModal({ shown, setShown }: { + shown: boolean; + setShown: (newValue: boolean) => void; +}) { + return ( + setShown(false)} + > + Hi + + ); +} From 49fc5e1559051891ca21d7c7bd62421e270967d5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 16:12:27 +0200 Subject: [PATCH 03/38] feat(mobile/tab_switcher): basic listing of tabs --- .../widgets/mobile_widgets/TabSwitcher.css | 6 +++ .../widgets/mobile_widgets/TabSwitcher.tsx | 39 +++++++++++++++++-- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index d9e49f89e..16121f8b0 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -3,4 +3,10 @@ max-height: unset; height: 95vh; } + + .tabs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1em; + } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 735dbfc98..89a40eda2 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -1,13 +1,15 @@ import "./TabSwitcher.css"; import { createPortal } from "preact/compat"; -import { useState } from "preact/hooks"; +import { useEffect, useState } from "preact/hooks"; +import appContext from "../../components/app_context"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; +import { useTriliumEvent } from "../react/hooks"; import Modal from "../react/Modal"; export default function TabSwitcher() { - const [ shown, setShown ] = useState(false); + const [ shown, setShown ] = useState(true); return ( <> @@ -21,7 +23,6 @@ export default function TabSwitcher() { ); } - function TabBarModal({ shown, setShown }: { shown: boolean; setShown: (newValue: boolean) => void; @@ -34,7 +35,37 @@ function TabBarModal({ shown, setShown }: { show={shown} onHidden={() => setShown(false)} > - Hi + ); } + +function TabBarModelContent() { + const mainNoteContexts = useMainNoteContexts(); + + useTriliumEvent("contextsReopened", () => { + console.log("Reopened contexts"); + }); + + return ( +
+ {mainNoteContexts.map((tabContext) => ( + {tabContext.note?.title} + ))} +
+ ); +} + +function useMainNoteContexts() { + const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts()); + + useTriliumEvent("newNoteContextCreated", ({ noteContext }) => { + if (noteContext.mainNtxId) return; + setNoteContexts([ + ...noteContexts, + noteContext + ]); + }); + + return noteContexts; +} From 3367bb2e5b166850c237cb2e9ac3a96ac9f3367b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 16:21:27 +0200 Subject: [PATCH 04/38] feat(mobile/tab_switcher): basic rendering of tab content --- .../collections/legacy/ListOrGridView.tsx | 2 +- .../widgets/mobile_widgets/TabSwitcher.css | 17 ++++++++++++ .../widgets/mobile_widgets/TabSwitcher.tsx | 26 +++++++++++++++++-- 3 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx index 61f79cc8d..61c7193a6 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx @@ -155,7 +155,7 @@ function NoteAttributes({ note }: { note: FNote }) { return ; } -function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: { +export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: { note: FNote; trim?: boolean; noChildrenList?: boolean; diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 16121f8b0..db4cc74a3 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -8,5 +8,22 @@ display: grid; grid-template-columns: 1fr 1fr; gap: 1em; + + .tab-card { + background: var(--card-background-color); + border-radius: 1em; + + header { + padding: 0.5em 1em; + border-bottom: 1px solid var(--main-border-color); + } + + .tab-preview { + height: 180px; + overflow: hidden; + font-size: 0.5em; + padding: 1em; + } + } } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 89a40eda2..5429e244c 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -4,6 +4,8 @@ import { createPortal } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; import appContext from "../../components/app_context"; +import NoteContext from "../../components/note_context"; +import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; import { useTriliumEvent } from "../react/hooks"; import Modal from "../react/Modal"; @@ -49,13 +51,33 @@ function TabBarModelContent() { return (
- {mainNoteContexts.map((tabContext) => ( - {tabContext.note?.title} + {mainNoteContexts.map((noteContext) => ( + ))}
); } +function Tab({ noteContext }: { + noteContext: NoteContext; +}) { + const { note } = noteContext; + + return ( +
+
{noteContext.note?.title}
+
+ {note && } +
+
+ ); +} + function useMainNoteContexts() { const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts()); From 1aae4098d6b54edf20939247629bab4db2dd018e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 16:27:35 +0200 Subject: [PATCH 05/38] feat(mobile/tab_switcher): --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 7 +++++++ apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 4 ---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index db4cc74a3..4769b833e 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -23,6 +23,13 @@ overflow: hidden; font-size: 0.5em; padding: 1em; + + p { margin-bottom: 0.2em;} + h2 { font-size: 1.20em; } + h3 { font-size: 1.15em; } + h4 { font-size: 1.10em; } + h5 { font-size: 1.05em} + h6 { font-size: 1em; } } } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 5429e244c..1f6cbed1a 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -45,10 +45,6 @@ function TabBarModal({ shown, setShown }: { function TabBarModelContent() { const mainNoteContexts = useMainNoteContexts(); - useTriliumEvent("contextsReopened", () => { - console.log("Reopened contexts"); - }); - return (
{mainNoteContexts.map((noteContext) => ( From 13aebc060e4de741e47c965d555ed0ebefddf79a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:01:22 +0200 Subject: [PATCH 06/38] feat(mobile/tab_switcher): display margins only for text --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 5 ++++- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 4769b833e..0540f2b54 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -22,7 +22,10 @@ height: 180px; overflow: hidden; font-size: 0.5em; - padding: 1em; + + &.type-text { + padding: 10px; + } p { margin-bottom: 0.2em;} h2 { font-size: 1.20em; } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 1f6cbed1a..c32b74228 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -1,5 +1,6 @@ import "./TabSwitcher.css"; +import clsx from "clsx"; import { createPortal } from "preact/compat"; import { useEffect, useState } from "preact/hooks"; @@ -62,7 +63,7 @@ function Tab({ noteContext }: { return (
{noteContext.note?.title}
-
+
{note && Date: Sat, 31 Jan 2026 17:04:19 +0200 Subject: [PATCH 07/38] feat(mobile/tab_switcher): clip note title --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 10 ++++++++++ apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 4 +++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 0540f2b54..c281c04d9 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -12,10 +12,20 @@ .tab-card { background: var(--card-background-color); border-radius: 1em; + min-width: 0; header { padding: 0.5em 1em; border-bottom: 1px solid var(--main-border-color); + display: flex; + overflow: hidden; + + .title { + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + font-size: 0.9em; + } } .tab-preview { diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index c32b74228..4ce8ed4bf 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -62,7 +62,9 @@ function Tab({ noteContext }: { return (
-
{noteContext.note?.title}
+
+ {noteContext.note?.title} +
{note && Date: Sat, 31 Jan 2026 17:07:32 +0200 Subject: [PATCH 08/38] fix(mobile/tab_switcher): no title if empty tab --- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 4ce8ed4bf..edbe10bbe 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -6,6 +6,7 @@ import { useEffect, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; +import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; import { useTriliumEvent } from "../react/hooks"; @@ -63,7 +64,7 @@ function Tab({ noteContext }: { return (
- {noteContext.note?.title} + {noteContext.note?.title ?? t("tab_row.new_tab")}
{note && Date: Sat, 31 Jan 2026 17:13:36 +0200 Subject: [PATCH 09/38] feat(mobile/tab_switcher): click to activate --- .../widgets/mobile_widgets/TabSwitcher.css | 2 ++ .../widgets/mobile_widgets/TabSwitcher.tsx | 23 ++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index c281c04d9..19360d8c1 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -32,6 +32,8 @@ height: 180px; overflow: hidden; font-size: 0.5em; + user-select: none; + pointer-events: none; &.type-text { padding: 10px; diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index edbe10bbe..cb7c79af1 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -2,7 +2,7 @@ import "./TabSwitcher.css"; import clsx from "clsx"; import { createPortal } from "preact/compat"; -import { useEffect, useState } from "preact/hooks"; +import { useCallback, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; @@ -31,6 +31,11 @@ function TabBarModal({ shown, setShown }: { shown: boolean; setShown: (newValue: boolean) => void; }) { + const selectTab = useCallback((noteContextToActivate: NoteContext) => { + appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); + setShown(false); + }, [ setShown ]); + return ( setShown(false)} > - + ); } -function TabBarModelContent() { +function TabBarModelContent({ selectTab }: { + selectTab: (noteContextToActivate: NoteContext) => void; +}) { const mainNoteContexts = useMainNoteContexts(); return (
{mainNoteContexts.map((noteContext) => ( - + ))}
); } -function Tab({ noteContext }: { +function Tab({ noteContext, selectTab }: { noteContext: NoteContext; + selectTab: (noteContextToActivate: NoteContext) => void; }) { const { note } = noteContext; return ( -
+
selectTab(noteContext)} + >
{noteContext.note?.title ?? t("tab_row.new_tab")}
From a02bbdc550ca0d269639cb5bdeda0ec0f3b9e0e7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:25:05 +0200 Subject: [PATCH 10/38] feat(mobile/tab_switcher): indicate active tab --- .../src/widgets/mobile_widgets/TabSwitcher.css | 9 +++++++++ .../src/widgets/mobile_widgets/TabSwitcher.tsx | 17 +++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 19360d8c1..f18865625 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -14,6 +14,15 @@ border-radius: 1em; min-width: 0; + &.active { + outline: 4px solid var(--more-accented-background-color); + background: var(--card-background-hover-color); + + .title { + font-weight: bold; + } + } + header { padding: 0.5em 1em; border-bottom: 1px solid var(--main-border-color); diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index cb7c79af1..572cfbbe5 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -9,7 +9,7 @@ import NoteContext from "../../components/note_context"; import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; -import { useTriliumEvent } from "../react/hooks"; +import { useActiveNoteContext, useTriliumEvent } from "../react/hooks"; import Modal from "../react/Modal"; export default function TabSwitcher() { @@ -53,25 +53,34 @@ function TabBarModelContent({ selectTab }: { selectTab: (noteContextToActivate: NoteContext) => void; }) { const mainNoteContexts = useMainNoteContexts(); + const activeNoteContext = useActiveNoteContext(); return (
{mainNoteContexts.map((noteContext) => ( - + ))}
); } -function Tab({ noteContext, selectTab }: { +function Tab({ noteContext, selectTab, activeNtxId }: { noteContext: NoteContext; selectTab: (noteContextToActivate: NoteContext) => void; + activeNtxId: string | null | undefined; }) { const { note } = noteContext; return (
selectTab(noteContext)} >
From 325b8b886cd45bb305085f072e79b2120cc08b5b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:25:33 +0200 Subject: [PATCH 11/38] fix(mobile/tab_switcher): clipped borders --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index f18865625..210dcc149 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -13,6 +13,7 @@ background: var(--card-background-color); border-radius: 1em; min-width: 0; + overflow: hidden; &.active { outline: 4px solid var(--more-accented-background-color); From bf0fc57493ad23257e74da87302d747960595860 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:29:24 +0200 Subject: [PATCH 12/38] feat(mobile/tab_switcher): display note icon --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 7 ++++++- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 5 ++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 210dcc149..94d80c022 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -25,10 +25,15 @@ } header { - padding: 0.5em 1em; + padding: 0.4em 0.5em; border-bottom: 1px solid var(--main-border-color); display: flex; overflow: hidden; + align-items: center; + + >.tn-icon { + margin-inline-end: 0.4em; + } .title { overflow: hidden; diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 572cfbbe5..ff481b2ed 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -9,7 +9,8 @@ import NoteContext from "../../components/note_context"; import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; -import { useActiveNoteContext, useTriliumEvent } from "../react/hooks"; +import { useActiveNoteContext, useNoteIcon, useTriliumEvent } from "../react/hooks"; +import Icon from "../react/Icon"; import Modal from "../react/Modal"; export default function TabSwitcher() { @@ -75,6 +76,7 @@ function Tab({ noteContext, selectTab, activeNtxId }: { activeNtxId: string | null | undefined; }) { const { note } = noteContext; + const iconClass = useNoteIcon(note); return (
selectTab(noteContext)} >
+ {noteContext.note?.title ?? t("tab_row.new_tab")}
From 43f147ec60f3f7ed023f309b64c829fbc60a7c28 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:31:30 +0200 Subject: [PATCH 13/38] feat(mobile/tab_switcher): improve display of content widget & search --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 94d80c022..d1ae03be2 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -54,6 +54,15 @@ padding: 10px; } + &.type-contentWidget, + &.type-search { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25em; + color: var(--muted-text-color); + } + p { margin-bottom: 0.2em;} h2 { font-size: 1.20em; } h3 { font-size: 1.15em; } From 6b70412f6e29d16643532126d6f882e3f2e631df Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 17:50:52 +0200 Subject: [PATCH 14/38] feat(mobile/tab_switcher): respect note color class --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 1 + apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index d1ae03be2..a8771562c 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -30,6 +30,7 @@ display: flex; overflow: hidden; align-items: center; + color: var(--custom-color, inherit); >.tn-icon { margin-inline-end: 0.4em; diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index ff481b2ed..c14cfa7e6 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -77,10 +77,11 @@ function Tab({ noteContext, selectTab, activeNtxId }: { }) { const { note } = noteContext; const iconClass = useNoteIcon(note); + const colorClass = note?.getColorClass() || ''; return (
selectTab(noteContext)} From 740b1093d7b96eb4fab982145c345930f9e0432a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 18:09:04 +0200 Subject: [PATCH 15/38] feat(mobile/tab_switcher): respect workspace background color --- apps/client/src/services/css_class_manager.ts | 4 ++-- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/apps/client/src/services/css_class_manager.ts b/apps/client/src/services/css_class_manager.ts index de1c98b87..06ff7b704 100644 --- a/apps/client/src/services/css_class_manager.ts +++ b/apps/client/src/services/css_class_manager.ts @@ -49,7 +49,7 @@ function createClassForColor(colorString: string | null) { return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue"); } -function parseColor(color: string) { +export function parseColor(color: string) { try { return Color(color.toLowerCase()); } catch (ex) { @@ -77,7 +77,7 @@ function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: numb } /** Returns the hue of the specified color, or undefined if the color is grayscale. */ -function getHue(color: ColorInstance) { +export function getHue(color: ColorInstance) { const hslColor = color.hsl(); if (hslColor.saturationl() > 0) { return hslColor.hue(); diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index a8771562c..f4fc31f3c 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -15,6 +15,11 @@ min-width: 0; overflow: hidden; + &.with-hue { + background-color: hsl(var(--bg-hue), 8.8%, 11.2%); + border-color: hsl(var(--bg-hue), 9.4%, 25.1%); + } + &.active { outline: 4px solid var(--more-accented-background-color); background: var(--card-background-hover-color); @@ -26,7 +31,7 @@ header { padding: 0.4em 0.5em; - border-bottom: 1px solid var(--main-border-color); + border-bottom: 1px solid rgba(150, 150, 150, 0.1); display: flex; overflow: hidden; align-items: center; From 2a38af5db60b7d2ac795c3e1cefee6bb8042fdc0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 18:25:01 +0200 Subject: [PATCH 16/38] feat(mobile/tab_switcher): scroll to active tab --- .../widgets/mobile_widgets/TabSwitcher.tsx | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index c14cfa7e6..e9efacecb 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -2,10 +2,12 @@ import "./TabSwitcher.css"; import clsx from "clsx"; import { createPortal } from "preact/compat"; -import { useCallback, useState } from "preact/hooks"; +import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; +import { getHue, parseColor } from "../../services/css_class_manager"; +import froca from "../../services/froca"; import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; @@ -32,6 +34,7 @@ function TabBarModal({ shown, setShown }: { shown: boolean; setShown: (newValue: boolean) => void; }) { + const [ fullyShown, setFullyShown ] = useState(false); const selectTab = useCallback((noteContextToActivate: NoteContext) => { appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); setShown(false); @@ -43,18 +46,33 @@ function TabBarModal({ shown, setShown }: { size="xl" title="Tabs" show={shown} - onHidden={() => setShown(false)} + onShown={() => setFullyShown(true)} + onHidden={() => { + setShown(false); + setFullyShown(false); + }} > - + ); } -function TabBarModelContent({ selectTab }: { +function TabBarModelContent({ selectTab, shown }: { + shown: boolean; selectTab: (noteContextToActivate: NoteContext) => void; }) { const mainNoteContexts = useMainNoteContexts(); const activeNoteContext = useActiveNoteContext(); + const tabRefs = useRef>({}); + + // Scroll to active tab. + useEffect(() => { + if (!shown || !activeNoteContext?.ntxId) return; + const correspondingEl = tabRefs.current[activeNoteContext.ntxId]; + requestAnimationFrame(() => { + correspondingEl?.scrollIntoView(); + }); + }, [ activeNoteContext, shown ]); return (
@@ -64,13 +82,15 @@ function TabBarModelContent({ selectTab }: { noteContext={noteContext} activeNtxId={activeNoteContext.ntxId} selectTab={selectTab} + containerRef={el => (tabRefs.current[noteContext.ntxId ?? ""] = el)} /> ))}
); } -function Tab({ noteContext, selectTab, activeNtxId }: { +function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { + containerRef: (el: HTMLDivElement | null) => void; noteContext: NoteContext; selectTab: (noteContextToActivate: NoteContext) => void; activeNtxId: string | null | undefined; @@ -78,15 +98,21 @@ function Tab({ noteContext, selectTab, activeNtxId }: { const { note } = noteContext; const iconClass = useNoteIcon(note); const colorClass = note?.getColorClass() || ''; + const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext); return (
selectTab(noteContext)} + style={{ + "--bg-hue": workspaceTabBackgroundColorHue + }} > -
+
{noteContext.note?.title ?? t("tab_row.new_tab")}
@@ -102,6 +128,23 @@ function Tab({ noteContext, selectTab, activeNtxId }: { ); } +function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) { + if (!noteContext.hoistedNoteId) return; + const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId); + if (!hoistedNote) return; + + const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor(); + if (!workspaceTabBackgroundColor) return; + + try { + const parsedColor = parseColor(workspaceTabBackgroundColor); + if (!parsedColor) return; + return getHue(parsedColor); + } catch (e) { + // Colors are non-critical, simply ignore. + } +} + function useMainNoteContexts() { const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts()); From 48db6e1756299ec59ba51fbe5c4a7566a5a082ae Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 18:40:01 +0200 Subject: [PATCH 17/38] feat(mobile/tab_switcher): button to close tab --- .../src/translations/en/translation.json | 3 +++ .../widgets/mobile_widgets/TabSwitcher.css | 5 +++++ .../widgets/mobile_widgets/TabSwitcher.tsx | 22 ++++++++++++------- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index d7d91b244..74ba61d28 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2271,5 +2271,8 @@ }, "platform_indicator": { "available_on": "Available on {{platform}}" + }, + "mobile_tab_switcher": { + "close_tab": "Close tab" } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index f4fc31f3c..cc4854137 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -46,6 +46,11 @@ text-overflow: ellipsis; text-wrap: nowrap; font-size: 0.9em; + flex-grow: 1; + } + + .icon-action { + flex-shrink: 0; } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index e9efacecb..86079fa84 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -2,7 +2,7 @@ import "./TabSwitcher.css"; import clsx from "clsx"; import { createPortal } from "preact/compat"; -import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; @@ -11,7 +11,8 @@ import froca from "../../services/froca"; import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; -import { useActiveNoteContext, useNoteIcon, useTriliumEvent } from "../react/hooks"; +import ActionButton from "../react/ActionButton"; +import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks"; import Icon from "../react/Icon"; import Modal from "../react/Modal"; @@ -115,6 +116,15 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
{noteContext.note?.title ?? t("tab_row.new_tab")} + { + // We are closing a tab, so we need to prevent propagation for click (activate tab). + e.stopPropagation(); + appContext.tabManager.removeNoteContext(noteContext.ntxId); + }} + />
{note && { - if (noteContext.mainNtxId) return; - setNoteContexts([ - ...noteContexts, - noteContext - ]); + useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ] , () => { + setNoteContexts(appContext.tabManager.getMainNoteContexts()); }); return noteContexts; From 0a7b2e330476bc1f92d758bcabb18656b4d51e78 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 18:49:25 +0200 Subject: [PATCH 18/38] feat(mobile/tab_switcher): integrate into launch bar --- apps/client/src/layouts/mobile_layout.tsx | 2 -- .../widgets/launch_bar/LauncherContainer.tsx | 3 +++ .../src/assets/translations/en/server.json | 3 ++- .../services/hidden_subtree_launcherbar.ts | 24 ++++++++++++------- packages/commons/src/lib/hidden_subtree.ts | 3 ++- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index 5c7270bfd..da66ffa13 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -13,7 +13,6 @@ import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js"; import ScreenContainer from "../widgets/mobile_widgets/screen_container.js"; import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js"; -import TabSwitcher from "../widgets/mobile_widgets/TabSwitcher.jsx"; import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx"; import NoteTitleWidget from "../widgets/note_title.js"; import NoteTreeWidget from "../widgets/note_tree.js"; @@ -185,7 +184,6 @@ export default class MobileLayout { .class("horizontal") .css("height", "53px") .child() - .child() .child() .id("launcher-pane")) ) diff --git a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx index 3450a4c01..d202feaf3 100644 --- a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx +++ b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx @@ -3,6 +3,7 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import froca from "../../services/froca"; import { isDesktop, isMobile } from "../../services/utils"; +import TabSwitcher from "../mobile_widgets/TabSwitcher"; import { useTriliumEvent } from "../react/hooks"; import { onWheelHorizontalScroll } from "../widget_utils"; import BookmarkButtons from "./BookmarkButtons"; @@ -97,6 +98,8 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) { return ; case "aiChatLauncher": return ; + case "mobileTabSwitcher": + return ; default: throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } diff --git a/apps/server/src/assets/translations/en/server.json b/apps/server/src/assets/translations/en/server.json index e6fa04b11..f6b23d489 100644 --- a/apps/server/src/assets/translations/en/server.json +++ b/apps/server/src/assets/translations/en/server.json @@ -356,7 +356,8 @@ "visible-launchers-title": "Visible Launchers", "user-guide": "User Guide", "localization": "Language & Region", - "inbox-title": "Inbox" + "inbox-title": "Inbox", + "tab-switcher-title": "Tab Switcher" }, "notes": { "new-note": "New note", diff --git a/apps/server/src/services/hidden_subtree_launcherbar.ts b/apps/server/src/services/hidden_subtree_launcherbar.ts index d68c10c1c..55a3b6f70 100644 --- a/apps/server/src/services/hidden_subtree_launcherbar.ts +++ b/apps/server/src/services/hidden_subtree_launcherbar.ts @@ -48,7 +48,7 @@ export default function buildLaunchBarConfig() { id: "_lbBackInHistory", ...sharedLaunchers.backInHistory }, - { + { id: "_lbForwardInHistory", ...sharedLaunchers.forwardInHistory }, @@ -59,12 +59,12 @@ export default function buildLaunchBarConfig() { command: "commandPalette", icon: "bx bx-chevron-right-square" }, - { + { id: "_lbBackendLog", title: t("hidden-subtree.backend-log-title"), type: "launcher", targetNoteId: "_backendLog", - icon: "bx bx-detail" + icon: "bx bx-detail" }, { id: "_zenMode", @@ -128,7 +128,7 @@ export default function buildLaunchBarConfig() { baseSize: "50", growthFactor: "0" }, - { + { id: "_lbBookmarks", title: t("hidden-subtree.bookmarks-title"), type: "launcher", @@ -139,7 +139,7 @@ export default function buildLaunchBarConfig() { id: "_lbToday", ...sharedLaunchers.openToday }, - { + { id: "_lbSpacer2", title: t("hidden-subtree.spacer-title"), type: "launcher", @@ -179,7 +179,11 @@ export default function buildLaunchBarConfig() { const mobileAvailableLaunchers: HiddenSubtreeItem[] = [ { id: "_lbMobileNewNote", ...sharedLaunchers.newNote }, - { id: "_lbMobileToday", ...sharedLaunchers.openToday } + { id: "_lbMobileToday", ...sharedLaunchers.openToday }, + { + id: "_lbMobileRecentChanges", + ...sharedLaunchers.recentChanges + } ]; const mobileVisibleLaunchers: HiddenSubtreeItem[] = [ @@ -203,8 +207,10 @@ export default function buildLaunchBarConfig() { ...sharedLaunchers.calendar }, { - id: "_lbMobileRecentChanges", - ...sharedLaunchers.recentChanges + id: "_lbMobileTabSwitcher", + title: t("hidden-subtree.tab-switcher-title"), + type: "launcher", + builtinWidget: "mobileTabSwitcher" } ]; @@ -214,4 +220,4 @@ export default function buildLaunchBarConfig() { mobileAvailableLaunchers, mobileVisibleLaunchers }; -} \ No newline at end of file +} diff --git a/packages/commons/src/lib/hidden_subtree.ts b/packages/commons/src/lib/hidden_subtree.ts index 2fcd68f11..91e46b708 100644 --- a/packages/commons/src/lib/hidden_subtree.ts +++ b/packages/commons/src/lib/hidden_subtree.ts @@ -45,7 +45,8 @@ export interface HiddenSubtreeItem { | "quickSearch" | "aiChatLauncher" | "commandPalette" - | "toggleZenMode"; + | "toggleZenMode" + | "mobileTabSwitcher"; command?: keyof typeof Command; /** * If set to true, then branches will be enforced to be in the correct place. From 39648b6df89507767678c4aa03b9f4d8e853472e Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:03:15 +0200 Subject: [PATCH 19/38] chore(mobile/tab_switcher): remove old tab bar --- 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 da66ffa13..dbe88ade8 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -179,7 +179,6 @@ export default class MobileLayout { new FlexContainer("column") .contentSized() .id("mobile-bottom-bar") - .child(new TabRowWidget().css("height", "40px")) .child(new FlexContainer("row") .class("horizontal") .css("height", "53px") From 5abd27f252da8ae3f7df2895f92255d6c721eff4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:04:47 +0200 Subject: [PATCH 20/38] chore(mobile/tab_switcher): improve modal fit when in browser --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index cc4854137..25cbb2238 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -1,7 +1,6 @@ .modal.tab-bar-modal { .modal-dialog { - max-height: unset; - height: 95vh; + height: 100%; } .tabs { From b6f107b85bcf76e3dd9e472e3427ed2219325f94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:08:29 +0200 Subject: [PATCH 21/38] feat(mobile/tab_switcher): display number of tabs in modal title --- apps/client/src/translations/en/translation.json | 3 ++- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 9 +++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 74ba61d28..e7f48008c 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2273,6 +2273,7 @@ "available_on": "Available on {{platform}}" }, "mobile_tab_switcher": { - "close_tab": "Close tab" + "title_one": "{{count}} tab", + "title_other": "{{count}} tabs" } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 86079fa84..31d08dd25 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -36,6 +36,7 @@ function TabBarModal({ shown, setShown }: { setShown: (newValue: boolean) => void; }) { const [ fullyShown, setFullyShown ] = useState(false); + const mainNoteContexts = useMainNoteContexts(); const selectTab = useCallback((noteContextToActivate: NoteContext) => { appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); setShown(false); @@ -45,7 +46,7 @@ function TabBarModal({ shown, setShown }: { setFullyShown(true)} onHidden={() => { @@ -53,16 +54,16 @@ function TabBarModal({ shown, setShown }: { setFullyShown(false); }} > - + ); } -function TabBarModelContent({ selectTab, shown }: { +function TabBarModelContent({ mainNoteContexts, selectTab, shown }: { + mainNoteContexts: NoteContext[]; shown: boolean; selectTab: (noteContextToActivate: NoteContext) => void; }) { - const mainNoteContexts = useMainNoteContexts(); const activeNoteContext = useActiveNoteContext(); const tabRefs = useRef>({}); From e8158aadec7330dfecef08e5429d094f5a4deb98 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:19:39 +0200 Subject: [PATCH 22/38] feat(mobile/tab_switcher): display number of tabs in launch bar --- .../src/widgets/launch_bar/launch_bar_widgets.tsx | 5 +++-- .../src/widgets/mobile_widgets/TabSwitcher.css | 14 ++++++++++++++ .../src/widgets/mobile_widgets/TabSwitcher.tsx | 9 ++++++--- apps/client/src/widgets/react/ActionButton.tsx | 11 ++++++----- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx index 0cb7c9906..bb4a053c8 100644 --- a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import { createContext } from "preact"; import { useContext } from "preact/hooks"; @@ -18,12 +19,12 @@ export interface LauncherNoteProps { launcherNote: FNote; } -export function LaunchBarActionButton(props: Omit) { +export function LaunchBarActionButton({ className, ...props }: Omit) { const { isHorizontalLayout } = useContext(LaunchBarContext); return ( setShown(true)} + data-tab-count={mainNoteContexts.length > 99 ? "∞" : mainNoteContexts.length} /> - {createPortal(, document.body)} + {createPortal(, document.body)} ); } -function TabBarModal({ shown, setShown }: { +function TabBarModal({ mainNoteContexts, shown, setShown }: { + mainNoteContexts: NoteContext[]; shown: boolean; setShown: (newValue: boolean) => void; }) { const [ fullyShown, setFullyShown ] = useState(false); - const mainNoteContexts = useMainNoteContexts(); const selectTab = useCallback((noteContextToActivate: NoteContext) => { appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); setShown(false); diff --git a/apps/client/src/widgets/react/ActionButton.tsx b/apps/client/src/widgets/react/ActionButton.tsx index ba5430f38..feb5972ef 100644 --- a/apps/client/src/widgets/react/ActionButton.tsx +++ b/apps/client/src/widgets/react/ActionButton.tsx @@ -1,10 +1,11 @@ -import { useEffect, useRef, useState } from "preact/hooks"; -import { CommandNames } from "../../components/app_context"; -import { useStaticTooltip } from "./hooks"; -import keyboard_actions from "../../services/keyboard_actions"; import { HTMLAttributes } from "preact"; +import { useEffect, useRef, useState } from "preact/hooks"; -export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu"> { +import { CommandNames } from "../../components/app_context"; +import keyboard_actions from "../../services/keyboard_actions"; +import { useStaticTooltip } from "./hooks"; + +export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu" | "style"> { text: string; titlePosition?: "top" | "right" | "bottom" | "left"; icon: string; From a486f5951e91f4d20ab5fa61af3624a4636f4e62 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:29:06 +0200 Subject: [PATCH 23/38] feat(mobile/tab_switcher): new tab button --- .../client/src/widgets/mobile_widgets/TabSwitcher.css | 9 +++++++++ .../client/src/widgets/mobile_widgets/TabSwitcher.tsx | 11 +++++++++++ 2 files changed, 20 insertions(+) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index e37b364a9..37a68c5b1 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -96,4 +96,13 @@ } } } + + .modal-footer { + .tn-link { + color: var(--main-text-color); + width: 40%; + text-align: center; + text-decoration: none; + } + } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 33ae93151..7f80360ee 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -14,6 +14,7 @@ import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; import ActionButton from "../react/ActionButton"; import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks"; import Icon from "../react/Icon"; +import LinkButton from "../react/LinkButton"; import Modal from "../react/Modal"; export default function TabSwitcher() { @@ -52,6 +53,16 @@ function TabBarModal({ mainNoteContexts, shown, setShown }: { title={t("mobile_tab_switcher.title", { count: mainNoteContexts.length})} show={shown} onShown={() => setFullyShown(true)} + footer={<> + { + appContext.triggerCommand("openNewTab"); + setShown(false); + }} + /> + } + scrollable onHidden={() => { setShown(false); setFullyShown(false); From bec7943e058fb8b1d16610ed71eb314c58cb2395 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:36:31 +0200 Subject: [PATCH 24/38] feat(mobile/tab_switcher): hide icon for new tab --- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 7f80360ee..c779a5804 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -129,7 +129,7 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { }} >
- + {note && } {noteContext.note?.title ?? t("tab_row.new_tab")} Date: Sat, 31 Jan 2026 19:43:53 +0200 Subject: [PATCH 25/38] feat(mobile/tab_switcher): improve display of collections --- .../widgets/mobile_widgets/TabSwitcher.css | 5 ++++ .../widgets/mobile_widgets/TabSwitcher.tsx | 25 ++++++++++++++----- .../note_bars/CollectionProperties.tsx | 5 +--- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 37a68c5b1..0bf9c51fd 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -78,6 +78,7 @@ padding: 10px; } + &.type-book, &.type-contentWidget, &.type-search { display: flex; @@ -87,6 +88,10 @@ color: var(--muted-text-color); } + .preview-placeholder { + font-size: 500%; + } + p { margin-bottom: 0.2em;} h2 { font-size: 1.20em; } h3 { font-size: 1.15em; } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index c779a5804..45d7ee575 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -11,6 +11,7 @@ import froca from "../../services/froca"; import { t } from "../../services/i18n"; import { NoteContent } from "../collections/legacy/ListOrGridView"; import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; +import { ICON_MAPPINGS } from "../note_bars/CollectionProperties"; import ActionButton from "../react/ActionButton"; import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks"; import Icon from "../react/Icon"; @@ -142,17 +143,29 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { />
- {note && } + {note?.type === "book" + ? + : note && }
); } +function PreviewPlaceholder({ icon}: { + icon: string; +}) { + return ( +
+ +
+ ); +} + function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) { if (!noteContext.hoistedNoteId) return; const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId); diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.tsx b/apps/client/src/widgets/note_bars/CollectionProperties.tsx index d466e813c..5dba675e6 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.tsx +++ b/apps/client/src/widgets/note_bars/CollectionProperties.tsx @@ -6,10 +6,7 @@ import { useContext, useRef } from "preact/hooks"; import { Fragment } from "preact/jsx-runtime"; import FNote from "../../entities/fnote"; -import { getHelpUrlForNote } from "../../services/in_app_help"; -import { openInAppHelpFromUrl } from "../../services/utils"; import { ViewTypeOptions } from "../collections/interface"; -import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; import FormTextBox from "../react/FormTextBox"; @@ -19,7 +16,7 @@ import { ParentComponent } from "../react/react_utils"; import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab"; -const ICON_MAPPINGS: Record = { +export const ICON_MAPPINGS: Record = { grid: "bx bxs-grid", list: "bx bx-list-ul", calendar: "bx bx-calendar", From 8f3545624ec25f63a0dc08e58839d991beffaafb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:46:15 +0200 Subject: [PATCH 26/38] feat(mobile/tab_switcher): improve display of empty tabs --- .../widgets/mobile_widgets/TabSwitcher.css | 3 +- .../widgets/mobile_widgets/TabSwitcher.tsx | 31 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 0bf9c51fd..f0a17fd04 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -80,7 +80,8 @@ &.type-book, &.type-contentWidget, - &.type-search { + &.type-search, + &.type-empty { display: flex; align-items: center; justify-content: center; diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 45d7ee575..c71673182 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; +import FNote from "../../entities/fnote"; import { getHue, parseColor } from "../../services/css_class_manager"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; @@ -143,19 +144,33 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { />
- {note?.type === "book" - ? - : note && } +
); } +function TabPreviewContent({ note }: { + note: FNote | null +}) { + if (!note) { + return ; + } + + if (note.type === "book") { + return ; + } + + return ( + + ); +} + function PreviewPlaceholder({ icon}: { icon: string; }) { From a72d4f425a63ed4cb4e5b0707b5d9d48e3680a78 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 19:54:42 +0200 Subject: [PATCH 27/38] feat(mobile/tab_switcher): add context menu item for closing all tabs --- .../src/translations/en/translation.json | 3 ++- .../widgets/mobile_widgets/TabSwitcher.tsx | 23 +++++++++++++++++- apps/client/src/widgets/react/Modal.tsx | 24 +++++++++---------- 3 files changed, 36 insertions(+), 14 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index e7f48008c..6bd13a334 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2274,6 +2274,7 @@ }, "mobile_tab_switcher": { "title_one": "{{count}} tab", - "title_other": "{{count}} tabs" + "title_other": "{{count}} tabs", + "more_options": "More options" } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index c71673182..994fc834d 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -4,9 +4,10 @@ import clsx from "clsx"; import { createPortal } from "preact/compat"; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; -import appContext from "../../components/app_context"; +import appContext, { CommandNames } from "../../components/app_context"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; +import contextMenu from "../../menus/context_menu"; import { getHue, parseColor } from "../../services/css_class_manager"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; @@ -55,6 +56,26 @@ function TabBarModal({ mainNoteContexts, shown, setShown }: { title={t("mobile_tab_switcher.title", { count: mainNoteContexts.length})} show={shown} onShown={() => setFullyShown(true)} + customTitleBarButtons={[ + { + iconClassName: "bx bx-dots-vertical-rounded", + title: t("mobile_tab_switcher.more_options"), + onClick(e) { + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items: [ + { title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-empty" }, + ], + selectMenuItemHandler: ({ command }) => { + if (command) { + appContext.triggerCommand(command); + } + } + }); + }, + } + ]} footer={<> void; + onClick: (e: MouseEvent) => void; } export interface ModalProps { @@ -80,7 +81,7 @@ export interface ModalProps { noFocus?: boolean; } -export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) { +export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) { const modalRef = useSyncedRef(externalModalRef); const modalInstanceRef = useRef(); const elementToFocus = useRef(); @@ -116,7 +117,7 @@ export default function Modal({ children, className, size, title, customTitleBar focus: !noFocus }).then(($widget) => { modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]); - }) + }); } else { modalInstanceRef.current?.hide(); } @@ -159,13 +160,12 @@ export default function Modal({ children, className, size, title, customTitleBar {titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => ( + className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)} + title={titleBarButton.title} + onClick={titleBarButton.onClick} /> ))} - +
From 3cb74bb84455bfff5bbdec194fd9f6dca903cbef Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:00:56 +0200 Subject: [PATCH 28/38] fix(mobile): context menu won't dismiss due to missing cover --- apps/client/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/index.html b/apps/client/index.html index e1db35332..12f653666 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -13,6 +13,7 @@ +
From 20eaa790795332e69f15e38c3ad31244bbb69ec5 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:04:00 +0200 Subject: [PATCH 29/38] fix(mobile): cover not working properly in modals --- apps/client/src/stylesheets/style.css | 2 +- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 17e431e54..a8d5c7500 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1255,7 +1255,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href inset-inline-start: 0; inset-inline-end: 0; bottom: 0; - z-index: 1000; + z-index: 2500; background: rgba(0, 0, 0, 0.1); } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 994fc834d..78107ec81 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -65,7 +65,10 @@ function TabBarModal({ mainNoteContexts, shown, setShown }: { x: e.pageX, y: e.pageY, items: [ - { title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-empty" }, + { title: t("tab_row.new_tab"), command: "openNewTab", uiIcon: "bx bx-plus" }, + { title: t("tab_row.reopen_last_tab"), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0 }, + { kind: "separator" }, + { title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-trash destructive-action-icon" }, ], selectMenuItemHandler: ({ command }) => { if (command) { From d2abde714fa2e928de7ce85537f4b7ccfe1a6e8b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:06:16 +0200 Subject: [PATCH 30/38] chore(mobile/tab_switcher): enforce same height --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index f0a17fd04..47736a72f 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -14,7 +14,7 @@ .modal.tab-bar-modal { .modal-dialog { - height: 100%; + min-height: 85vh; } .tabs { From e7f356b87c06064b2bb918489b2693d12d2d0fe7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:21:11 +0200 Subject: [PATCH 31/38] feat(mobile/tab_switcher): display note splits --- .../widgets/mobile_widgets/TabSwitcher.css | 17 +++++++- .../widgets/mobile_widgets/TabSwitcher.tsx | 42 +++++++++++-------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 47736a72f..4518d76c2 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -27,6 +27,9 @@ border-radius: 1em; min-width: 0; overflow: hidden; + height: 200px; + display: flex; + flex-direction: column; &.with-hue { background-color: hsl(var(--bg-hue), 8.8%, 11.2%); @@ -49,6 +52,11 @@ overflow: hidden; align-items: center; color: var(--custom-color, inherit); + flex-shrink: 0; + + &:not(:first-of-type) { + border-top: 1px solid rgba(150, 150, 150, 0.1); + } >.tn-icon { margin-inline-end: 0.4em; @@ -68,7 +76,8 @@ } .tab-preview { - height: 180px; + flex-grow: 1; + height: 100%; overflow: hidden; font-size: 0.5em; user-select: none; @@ -100,6 +109,12 @@ h5 { font-size: 1.05em} h6 { font-size: 1em; } } + + &.with-split { + .preview-placeholder { + font-size: 250%; + } + } } } diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index 78107ec81..c3d32129c 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -1,7 +1,7 @@ import "./TabSwitcher.css"; import clsx from "clsx"; -import { createPortal } from "preact/compat"; +import { createPortal, Fragment } from "preact/compat"; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import appContext, { CommandNames } from "../../components/app_context"; @@ -141,35 +141,41 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { const iconClass = useNoteIcon(note); const colorClass = note?.getColorClass() || ''; const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext); + const subContexts = noteContext.getSubContexts(); return (
1 })} onClick={() => selectTab(noteContext)} style={{ "--bg-hue": workspaceTabBackgroundColorHue }} > -
- {note && } - {noteContext.note?.title ?? t("tab_row.new_tab")} - { - // We are closing a tab, so we need to prevent propagation for click (activate tab). - e.stopPropagation(); - appContext.tabManager.removeNoteContext(noteContext.ntxId); - }} - /> -
-
- -
+ {subContexts.map(subContext => ( + +
+ {subContext.note && } + {subContext.note?.title ?? t("tab_row.new_tab")} + {subContext.isMainContext() && { + // We are closing a tab, so we need to prevent propagation for click (activate tab). + e.stopPropagation(); + appContext.tabManager.removeNoteContext(subContext.ntxId); + }} + />} +
+
+ +
+
+ ))}
); } From e9b826e49838b0bf93f6c7ca929fad77e8ff6476 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:22:28 +0200 Subject: [PATCH 32/38] chore(mobile/tab_switcher): stop auto-showing --- apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx index c3d32129c..6ee84f046 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -21,7 +21,7 @@ import LinkButton from "../react/LinkButton"; import Modal from "../react/Modal"; export default function TabSwitcher() { - const [ shown, setShown ] = useState(true); + const [ shown, setShown ] = useState(false); const mainNoteContexts = useMainNoteContexts(); return ( @@ -225,6 +225,7 @@ function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) { return getHue(parsedColor); } catch (e) { // Colors are non-critical, simply ignore. + console.warn(e); } } From a5306b2067542aee21c8e69d145c722aefb8d0fd Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:35:16 +0200 Subject: [PATCH 33/38] fix(mobile): modals on tablet view --- apps/client/src/stylesheets/style.css | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index a8d5c7500..9ac3995d0 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -224,10 +224,6 @@ body.mobile .modal .modal-dialog { width: 100%; } -body.mobile .modal .modal-content { - border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0; -} - .component { contain: size; } @@ -1614,6 +1610,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { body.mobile .modal-content { overflow-y: auto; + border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0; } body.mobile .modal-footer { @@ -1650,6 +1647,17 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { } } +@media (min-width: 992px) { + .modal-dialog { + margin: var(--bs-modal-margin); + max-width: 80%; + } + + .modal-content { + height: 100%; + } +} + /* Mobile, tablet mode */ @media (min-width: 992px) { body.mobile #root-widget { From 0d99cf9fb9e8283da0826a54f34701d774f371c0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:41:53 +0200 Subject: [PATCH 34/38] chore(mobile/tab_switcher): improve layout on tablet view --- apps/client/src/widgets/mobile_widgets/TabSwitcher.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css index 4518d76c2..f401ee61b 100644 --- a/apps/client/src/widgets/mobile_widgets/TabSwitcher.css +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.css @@ -22,6 +22,10 @@ grid-template-columns: 1fr 1fr; gap: 1em; + @media (min-width: 850px) { + grid-template-columns: 1fr 1fr 1fr; + } + .tab-card { background: var(--card-background-color); border-radius: 1em; From ff80154fda4518a55c7494518813d11f30075d30 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 20:47:07 +0200 Subject: [PATCH 35/38] chore(mobile/tab_switcher): address requested changes --- apps/client/src/stylesheets/style.css | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 9ac3995d0..69632dd29 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1647,17 +1647,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { } } -@media (min-width: 992px) { - .modal-dialog { - margin: var(--bs-modal-margin); - max-width: 80%; - } - - .modal-content { - height: 100%; - } -} - /* Mobile, tablet mode */ @media (min-width: 992px) { body.mobile #root-widget { @@ -1677,6 +1666,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { #detail-container { background: var(--main-background-color); } + + .modal-dialog { + margin: var(--bs-modal-margin); + max-width: 80%; + } + + .modal-content { + height: 100%; + } } @media (max-width: 991px) { From 5f20ce87a701c5d475249758f312a53da67fcd2f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 31 Jan 2026 21:17:08 +0200 Subject: [PATCH 36/38] chore(mobile/tab_switcher): bypass weird regression in typecheck regarding React types --- .../client/src/widgets/layout/InlineTitle.tsx | 1 + apps/client/src/widgets/note_icon.tsx | 7 ++- apps/client/src/widgets/react/Modal.tsx | 5 +- .../widgets/type_widgets/options/other.tsx | 58 ++++++++++--------- .../type_widgets/options/text_notes.tsx | 1 + 5 files changed, 38 insertions(+), 34 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 907090253..f6e5a5155 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -5,6 +5,7 @@ import { Tooltip } from "bootstrap"; import clsx from "clsx"; import { ComponentChild } from "preact"; import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; +import type React from "react"; import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; diff --git a/apps/client/src/widgets/note_icon.tsx b/apps/client/src/widgets/note_icon.tsx index 7bbbd3e34..51204b015 100644 --- a/apps/client/src/widgets/note_icon.tsx +++ b/apps/client/src/widgets/note_icon.tsx @@ -6,6 +6,7 @@ import clsx from "clsx"; import { t } from "i18next"; import { CSSProperties, RefObject } from "preact"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import type React from "react"; import { CellComponentProps, Grid } from "react-window"; import FNote from "../entities/fnote"; @@ -153,10 +154,10 @@ function NoteIconList({ note, dropdownRef }: { function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{ filteredIcons: IconWithName[]; -}>): React.JSX.Element { +}>) { const iconIndex = rowIndex * 12 + columnIndex; const iconData = filteredIcons[iconIndex] as IconWithName | undefined; - if (!iconData) return <>; + if (!iconData) return <> as React.ReactElement; const { id, terms, iconPack } = iconData; return ( @@ -166,7 +167,7 @@ function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellCompo title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })} style={style as CSSProperties} /> - ); + ) as React.ReactElement; } function IconFilterContent({ filterByPrefix, setFilterByPrefix }: { diff --git a/apps/client/src/widgets/react/Modal.tsx b/apps/client/src/widgets/react/Modal.tsx index 917c3bd99..e7a7c721f 100644 --- a/apps/client/src/widgets/react/Modal.tsx +++ b/apps/client/src/widgets/react/Modal.tsx @@ -1,9 +1,8 @@ import { Modal as BootstrapModal } from "bootstrap"; import clsx from "clsx"; -import { ComponentChildren } from "preact"; -import type { CSSProperties, RefObject } from "preact/compat"; +import { ComponentChildren, CSSProperties, RefObject } from "preact"; import { memo } from "preact/compat"; -import { useEffect, useMemo,useRef } from "preact/hooks"; +import { useEffect, useMemo, useRef } from "preact/hooks"; import { openDialog } from "../../services/dialog"; import { t } from "../../services/i18n"; diff --git a/apps/client/src/widgets/type_widgets/options/other.tsx b/apps/client/src/widgets/type_widgets/options/other.tsx index 6ac92b420..e6813f8d2 100644 --- a/apps/client/src/widgets/type_widgets/options/other.tsx +++ b/apps/client/src/widgets/type_widgets/options/other.tsx @@ -1,20 +1,22 @@ +import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons"; +import { useMemo } from "preact/hooks"; +import type React from "react"; import { Trans } from "react-i18next"; + import { t } from "../../../services/i18n"; +import search from "../../../services/search"; import server from "../../../services/server"; import toast from "../../../services/toast"; +import { isElectron } from "../../../services/utils"; import Button from "../../react/Button"; -import FormText from "../../react/FormText"; -import OptionsSection from "./components/OptionsSection"; -import TimeSelector from "./components/TimeSelector"; -import { useMemo } from "preact/hooks"; -import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks"; -import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons"; import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; -import search from "../../../services/search"; -import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; import FormSelect from "../../react/FormSelect"; -import { isElectron } from "../../../services/utils"; +import FormText from "../../react/FormText"; +import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; +import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks"; +import OptionsSection from "./components/OptionsSection"; +import TimeSelector from "./components/TimeSelector"; export default function OtherSettings() { return ( @@ -31,7 +33,7 @@ export default function OtherSettings() { - ) + ); } function SearchEngineSettings() { @@ -82,7 +84,7 @@ function SearchEngineSettings() { /> - ) + ); } function TrayOptionsSettings() { @@ -97,7 +99,7 @@ function TrayOptionsSettings() { onChange={trayEnabled => setDisableTray(!trayEnabled)} /> - ) + ); } function NoteErasureTimeout() { @@ -105,13 +107,13 @@ function NoteErasureTimeout() { {t("note_erasure_timeout.note_erasure_description")} - {t("note_erasure_timeout.manual_erasing_description")} - +