From 2a9558e9c555bb6092de4ce3db43611f96c7a522 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 13:17:54 +0200 Subject: [PATCH 01/14] style(ribbon): make icons slightly bigger --- apps/client/src/widgets/ribbon/style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 07619c89d..26c64c9d1 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -36,10 +36,11 @@ display: flex; align-items: center; font-size: 0.9em; + padding-top: 2px; } .ribbon-tab-title .bx { - font-size: 125%; + font-size: 150%; position: relative; } From 346ad1e8a34a70c67258d6eb2aae551962e17866 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:06:57 +0200 Subject: [PATCH 02/14] feat(tab_navigation): add the buttons on vertical layout --- apps/client/src/layouts/desktop_layout.tsx | 8 +++++++- .../src/widgets/TabHistoryNavigationButtons.css | 7 +++++++ .../src/widgets/TabHistoryNavigationButtons.tsx | 11 +++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/widgets/TabHistoryNavigationButtons.css create mode 100644 apps/client/src/widgets/TabHistoryNavigationButtons.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 41a6bb68f..f446ef25a 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -45,6 +45,7 @@ import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx"; import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; import Breadcrumb from "../widgets/Breadcrumb.jsx"; +import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; export default class DesktopLayout { @@ -102,7 +103,12 @@ export default class DesktopLayout { new FlexContainer("column") .id("rest-pane") .css("flex-grow", "1") - .optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, ).css("height", "40px")) + .optChild(!fullWidthTabBar, + new FlexContainer("row") + .child() + .child(new TabRowWidget()) + .optChild(customTitleBarButtons, ) + .css("height", "40px")) .child( new FlexContainer("row") .filling() diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.css b/apps/client/src/widgets/TabHistoryNavigationButtons.css new file mode 100644 index 000000000..4d040b9a5 --- /dev/null +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.css @@ -0,0 +1,7 @@ +.component.tab-history-navigation-buttons { + contain: none; + flex-shrink: 0; + display: flex; + align-items: center; + margin-inline-end: 0.5em; +} diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx new file mode 100644 index 000000000..cac468c2d --- /dev/null +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -0,0 +1,11 @@ +import ActionButton from "./react/ActionButton"; +import "./TabHistoryNavigationButtons.css"; + +export default function TabHistoryNavigationButtons() { + return ( +
+ + +
+ ) +} From e3f5b3535aa93c3cd0303772fc728b17d40fcbae Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:09:31 +0200 Subject: [PATCH 03/14] feat(tab_navigation): functional back/forward buttons --- .../src/translations/en/translation.json | 4 ++++ .../widgets/TabHistoryNavigationButtons.tsx | 18 ++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index fea8ca13f..24e30c6a9 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2117,5 +2117,9 @@ "unknown_http_error_title": "Communication error with the server", "unknown_http_error_content": "Status code: {{statusCode}}\nURL: {{method}} {{url}}\nMessage: {{message}}", "traefik_blocks_requests": "If you are using the Traefik reverse proxy, it introduced a breaking change which affects the communication with the server." + }, + "tab_history_navigation_buttons": { + "go-back": "Go back to previous note", + "go-forward": "Go forward to next note" } } diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx index cac468c2d..e3972f0df 100644 --- a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -1,11 +1,21 @@ -import ActionButton from "./react/ActionButton"; import "./TabHistoryNavigationButtons.css"; +import { t } from "../services/i18n"; +import ActionButton from "./react/ActionButton"; + export default function TabHistoryNavigationButtons() { return (
- - + +
- ) + ); } From 9e099444b6478e40abb7390b90573de547eda81f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:23:06 +0200 Subject: [PATCH 04/14] feat(tab_navigation): functional context menu --- .../widgets/TabHistoryNavigationButtons.tsx | 8 ++ .../widgets/launch_bar/HistoryNavigation.tsx | 125 +++++++++--------- 2 files changed, 68 insertions(+), 65 deletions(-) diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx index e3972f0df..d0e6afced 100644 --- a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -2,19 +2,27 @@ import "./TabHistoryNavigationButtons.css"; import { t } from "../services/i18n"; import ActionButton from "./react/ActionButton"; +import { useCallback, useMemo } from "preact/hooks"; +import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation"; +import { dynamicRequire } from "../services/utils"; export default function TabHistoryNavigationButtons() { + const webContents = useMemo(() => dynamicRequire("@electron/remote").getCurrentWebContents(), []); + const onContextMenu = handleHistoryContextMenu(webContents); + return (
); diff --git a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx index f9ea51c57..e861e3358 100644 --- a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx +++ b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx @@ -1,11 +1,12 @@ -import { useEffect, useRef } from "preact/hooks"; -import FNote from "../../entities/fnote"; -import { dynamicRequire, isElectron } from "../../services/utils"; -import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; import type { WebContents } from "electron"; +import { useCallback, useMemo } from "preact/hooks"; + +import FNote from "../../entities/fnote"; import contextMenu, { MenuCommandItem } from "../../menus/context_menu"; -import tree from "../../services/tree"; import link from "../../services/link"; +import tree from "../../services/tree"; +import { dynamicRequire } from "../../services/utils"; +import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets"; interface HistoryNavigationProps { launcherNote: FNote; @@ -16,71 +17,65 @@ const HISTORY_LIMIT = 20; export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) { const { icon, title } = useLauncherIconAndTitle(launcherNote); - const webContentsRef = useRef(null); - - useEffect(() => { - if (isElectron()) { - const webContents = dynamicRequire("@electron/remote").getCurrentWebContents(); - // without this, the history is preserved across frontend reloads - webContents?.clearHistory(); - webContentsRef.current = webContents; - } - }, []); + const webContents = useMemo(() => dynamicRequire("@electron/remote").getCurrentWebContents(), []); return ( { - e.preventDefault(); - - const webContents = webContentsRef.current; - if (!webContents || webContents.navigationHistory.length() < 2) { - return; - } - - let items: MenuCommandItem[] = []; - - const history = webContents.navigationHistory.getAllEntries(); - const activeIndex = webContents.navigationHistory.getActiveIndex(); - - for (const idx in history) { - const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); - if (!notePath) continue; - - const title = await tree.getNotePathTitle(notePath); - - items.push({ - title, - command: idx, - uiIcon: - parseInt(idx) === activeIndex - ? "bx bx-radio-circle-marked" // compare with type coercion! - : parseInt(idx) < activeIndex - ? "bx bx-left-arrow-alt" - : "bx bx-right-arrow-alt" - }); - } - - items.reverse(); - - if (items.length > HISTORY_LIMIT) { - items = items.slice(0, HISTORY_LIMIT); - } - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items, - selectMenuItemHandler: (item: MenuCommandItem) => { - if (item && item.command && webContents) { - const idx = parseInt(item.command, 10); - webContents.navigationHistory.goToIndex(idx); - } - } - }); - }} + onContextMenu={handleHistoryContextMenu(webContents)} /> - ) + ); +} + +export function handleHistoryContextMenu(webContents: WebContents) { + return async (e: MouseEvent) => { + e.preventDefault(); + + if (!webContents || webContents.navigationHistory.length() < 2) { + return; + } + + let items: MenuCommandItem[] = []; + + const history = webContents.navigationHistory.getAllEntries(); + const activeIndex = webContents.navigationHistory.getActiveIndex(); + + for (const idx in history) { + const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); + if (!notePath) continue; + + const title = await tree.getNotePathTitle(notePath); + + items.push({ + title, + command: idx, + uiIcon: + parseInt(idx, 10) === activeIndex + ? "bx bx-radio-circle-marked" // compare with type coercion! + : parseInt(idx, 10) < activeIndex + ? "bx bx-left-arrow-alt" + : "bx bx-right-arrow-alt" + }); + } + + items.reverse(); + + if (items.length > HISTORY_LIMIT) { + items = items.slice(0, HISTORY_LIMIT); + } + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items, + selectMenuItemHandler: (item: MenuCommandItem) => { + if (item && item.command && webContents) { + const idx = parseInt(item.command, 10); + webContents.navigationHistory.goToIndex(idx); + } + } + }); + }; } From 5a668ede01c95505f1fa649a034a3726a0956bb8 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:24:24 +0200 Subject: [PATCH 05/14] chore(tab_navigation): reintroduce history cleaning --- apps/client/src/desktop.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/client/src/desktop.ts b/apps/client/src/desktop.ts index e77ba845b..b88eb8d4a 100644 --- a/apps/client/src/desktop.ts +++ b/apps/client/src/desktop.ts @@ -64,6 +64,9 @@ function initOnElectron() { if (options.get("nativeTitleBarVisible") !== "true") { initTitleBarButtons(style, currentWindow); } + + // Clear navigation history on frontend refresh. + currentWindow.webContents.navigationHistory.clear(); } function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) { From 4b2a4b8f7be414d0b375dc4a74e7135df23de969 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:29:45 +0200 Subject: [PATCH 06/14] feat(tab_navigation): reflect state of history by disabling the buttons --- .../widgets/TabHistoryNavigationButtons.tsx | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx index d0e6afced..76fa80b6e 100644 --- a/apps/client/src/widgets/TabHistoryNavigationButtons.tsx +++ b/apps/client/src/widgets/TabHistoryNavigationButtons.tsx @@ -1,14 +1,16 @@ import "./TabHistoryNavigationButtons.css"; +import { useEffect, useMemo, useState } from "preact/hooks"; + import { t } from "../services/i18n"; -import ActionButton from "./react/ActionButton"; -import { useCallback, useMemo } from "preact/hooks"; -import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation"; import { dynamicRequire } from "../services/utils"; +import { handleHistoryContextMenu } from "./launch_bar/HistoryNavigation"; +import ActionButton from "./react/ActionButton"; export default function TabHistoryNavigationButtons() { const webContents = useMemo(() => dynamicRequire("@electron/remote").getCurrentWebContents(), []); const onContextMenu = handleHistoryContextMenu(webContents); + const { canGoBack, canGoForward } = useBackForwardState(webContents); return (
@@ -17,13 +19,37 @@ export default function TabHistoryNavigationButtons() { text={t("tab_history_navigation_buttons.go-back")} triggerCommand="backInNoteHistory" onContextMenu={onContextMenu} + disabled={!canGoBack} />
); } + +function useBackForwardState(webContents: Electron.WebContents) { + const [ canGoBack, setCanGoBack ] = useState(webContents.navigationHistory.canGoBack()); + const [ canGoForward, setCanGoForward ] = useState(webContents.navigationHistory.canGoForward()); + + useEffect(() => { + const updateNavigationState = () => { + setCanGoBack(webContents.navigationHistory.canGoBack()); + setCanGoForward(webContents.navigationHistory.canGoForward()); + }; + + webContents.on("did-navigate", updateNavigationState); + webContents.on("did-navigate-in-page", updateNavigationState); + + return () => { + webContents.removeListener("did-navigate", updateNavigationState); + webContents.removeListener("did-navigate-in-page", updateNavigationState); + }; + }, [ webContents ]); + + return { canGoBack, canGoForward }; +} From 7ee060b228bf86da60ab5164fdfd357c30e571d4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:41:17 +0200 Subject: [PATCH 07/14] feat(tab_navigation): improve indicator for current item in context menu --- .../src/widgets/launch_bar/HistoryNavigation.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx index e861e3358..e212d9af7 100644 --- a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx +++ b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx @@ -47,16 +47,16 @@ export function handleHistoryContextMenu(webContents: WebContents) { if (!notePath) continue; const title = await tree.getNotePathTitle(notePath); + const index = parseInt(idx, 10); items.push({ title, command: idx, - uiIcon: - parseInt(idx, 10) === activeIndex - ? "bx bx-radio-circle-marked" // compare with type coercion! - : parseInt(idx, 10) < activeIndex - ? "bx bx-left-arrow-alt" - : "bx bx-right-arrow-alt" + checked: index === activeIndex, + enabled: index !== activeIndex, + uiIcon: index !== activeIndex && index < activeIndex + ? "bx bx-left-arrow-alt" + : "bx bx-right-arrow-alt" }); } From 5c8132088f3f23eb0ff366ff157fbe5d392549a9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Tue, 9 Dec 2025 14:49:19 +0200 Subject: [PATCH 08/14] feat(client): use chevrons to display note path --- apps/client/src/services/tree.ts | 4 +++- apps/client/src/widgets/ribbon/NotePathsTab.tsx | 11 ++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/client/src/services/tree.ts b/apps/client/src/services/tree.ts index cfa210600..00e88cf6d 100644 --- a/apps/client/src/services/tree.ts +++ b/apps/client/src/services/tree.ts @@ -4,6 +4,8 @@ import froca from "./froca.js"; import hoistedNoteService from "./hoisted_note.js"; import appContext from "../components/app_context.js"; +export const NOTE_PATH_TITLE_SEPARATOR = " › "; + async function resolveNotePath(notePath: string, hoistedNoteId = "root") { const runPath = await resolveNotePathToSegments(notePath, hoistedNoteId); @@ -254,7 +256,7 @@ async function getNotePathTitle(notePath: string) { const titlePath = await getNotePathTitleComponents(notePath); - return titlePath.join(" / "); + return titlePath.join(NOTE_PATH_TITLE_SEPARATOR); } async function getNoteTitleWithPathAsSuffix(notePath: string) { diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.tsx b/apps/client/src/widgets/ribbon/NotePathsTab.tsx index 00249b0b4..0b997cfb2 100644 --- a/apps/client/src/widgets/ribbon/NotePathsTab.tsx +++ b/apps/client/src/widgets/ribbon/NotePathsTab.tsx @@ -6,6 +6,7 @@ import { useEffect, useMemo, useState } from "preact/hooks"; import { NotePathRecord } from "../../entities/fnote"; import NoteLink from "../react/NoteLink"; import { joinElements } from "../react/react_utils"; +import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree"; export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) { const [ sortedNotePaths, setSortedNotePaths ] = useState(); @@ -33,7 +34,7 @@ export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabConte
{sortedNotePaths?.length ? t("note_paths.intro_placed") : t("note_paths.intro_not_placed")}
- +
    {sortedNotePaths?.length ? sortedNotePaths.map(sortedNotePath => ( )) : undefined}
- +