From 78b9c94829acbcd82ba7a5476fafde2b7ea040e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 00:13:25 +0200 Subject: [PATCH 01/52] chore(layout/status_bar): create empty component --- apps/client/src/layouts/desktop_layout.tsx | 4 +++- apps/client/src/widgets/layout/StatusBar.css | 3 +++ apps/client/src/widgets/layout/StatusBar.tsx | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 apps/client/src/widgets/layout/StatusBar.css create mode 100644 apps/client/src/widgets/layout/StatusBar.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 9c0e02001..8815e113d 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -53,6 +53,7 @@ import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibb import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; import NoteStatusBar from "../widgets/NoteStatusBar.jsx"; +import StatusBar from "../widgets/layout/StatusBar.jsx"; export default class DesktopLayout { @@ -134,6 +135,7 @@ export default class DesktopLayout { .filling() .collapsible() .id("center-pane") + .optChild(isNewLayout, ) .child( new SplitNoteContainer(() => new NoteWrapperWidget() @@ -152,7 +154,6 @@ export default class DesktopLayout { ) .optChild(!isFloatingTitlebar, titleRow) .optChild(!isNewLayout, ) - .optChild(isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) .child() .child( @@ -183,6 +184,7 @@ export default class DesktopLayout { )) + .optChild(isNewLayout, ) ) ) .child(...this.customWidgets.get("center-pane")) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css new file mode 100644 index 000000000..d14e1f2a6 --- /dev/null +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -0,0 +1,3 @@ +.component.status-bar { + contain: none; +} diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx new file mode 100644 index 000000000..d537c5802 --- /dev/null +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -0,0 +1,9 @@ +import "./StatusBar.css"; + +export default function StatusBar() { + return ( +
+ Status bar goes here. +
+ ); +} From 9ee3c4848556613ee275b17d74c21024fa6a960d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 00:15:58 +0200 Subject: [PATCH 02/52] chore(layout): relocate ribbon on the top temporarily --- apps/client/src/layouts/desktop_layout.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 8815e113d..d2e88740b 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -154,6 +154,11 @@ export default class DesktopLayout { ) .optChild(!isFloatingTitlebar, titleRow) .optChild(!isNewLayout, ) + .optChild(isNewLayout, ( + + + + )) .child(new WatchedFileUpdateStatusWidget()) .child() .child( @@ -179,11 +184,6 @@ export default class DesktopLayout { ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC ...this.customWidgets.get("note-detail-pane") ) - .optChild(isNewLayout, ( - - - - )) .optChild(isNewLayout, ) ) ) From 2f44b9dc59cb0d2e4ab87ee4fa5d38be8f2b0532 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 00:21:40 +0200 Subject: [PATCH 03/52] feat(layout/status_bar): integrate breadcrumbs --- apps/client/src/layouts/desktop_layout.tsx | 1 - apps/client/src/widgets/Breadcrumb.css | 128 +++++++++---------- apps/client/src/widgets/layout/StatusBar.css | 2 + apps/client/src/widgets/layout/StatusBar.tsx | 5 +- 4 files changed, 69 insertions(+), 67 deletions(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index d2e88740b..b25e7c70c 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -143,7 +143,6 @@ export default class DesktopLayout { new FlexContainer("row") .class("breadcrumb-row") .cssBlock(".breadcrumb-row > * { margin: 5px; }") - .child() .optChild(isNewLayout, ) .child() .child() diff --git a/apps/client/src/widgets/Breadcrumb.css b/apps/client/src/widgets/Breadcrumb.css index 2bb58281f..ef28d42ee 100644 --- a/apps/client/src/widgets/Breadcrumb.css +++ b/apps/client/src/widgets/Breadcrumb.css @@ -1,9 +1,7 @@ .breadcrumb-row { position: relative; - height: 30px; min-height: 30px; align-items: center; - padding: 10px; container-type: inline-size; @container (max-width: 700px) { @@ -47,72 +45,72 @@ margin: 0; } } + + .breadcrumb { + display: flex; + margin: 0; + align-items: center; + font-size: 0.9em; + gap: 0.25em; + flex-wrap: nowrap; + overflow: hidden; + max-width: 85%; + + > span, + > span > span { + display: flex; + align-items: center; + min-width: 0; + + a { + color: inherit; + text-decoration: none; + min-width: 0; + max-width: 150px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; + flex-shrink: 2; + } + } + + > span:last-of-type a { + max-width: 300px; + flex-shrink: 1; + } + + ul { + flex-direction: column; + list-style-type: none; + margin: 0; + padding: 0; + } + + .dropdown-item span, + .dropdown-item strong, + .breadcrumb-last-item { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + display: block; + max-width: 300px; + } + + .breadcrumb-last-item { + text-decoration: none; + color: unset; + cursor: text; + } + + input { + padding: 0 10px; + width: 200px; + } + } } body.experimental-feature-new-layout .breadcrumb-row { padding-inline-end: 0; } -.component.breadcrumb { - contain: none; - display: flex; - margin: 0; - align-items: center; - font-size: 0.9em; - gap: 0.25em; - flex-wrap: nowrap; - overflow: hidden; - max-width: 85%; - - > span, - > span > span { - display: flex; - align-items: center; - min-width: 0; - - a { - color: inherit; - text-decoration: none; - min-width: 0; - max-width: 150px; - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - display: block; - flex-shrink: 2; - } - } - - > span:last-of-type a { - max-width: 300px; - flex-shrink: 1; - } - - ul { - flex-direction: column; - list-style-type: none; - margin: 0; - padding: 0; - } - - .dropdown-item span, - .dropdown-item strong, - .breadcrumb-last-item { - text-overflow: ellipsis; - white-space: nowrap; - overflow: hidden; - display: block; - max-width: 300px; - } - - .breadcrumb-last-item { - text-decoration: none; - color: unset; - cursor: text; - } - - input { - padding: 0 10px; - width: 200px; - } -} diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index d14e1f2a6..8ece8cc27 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -1,3 +1,5 @@ .component.status-bar { contain: none; + border-top: 1px solid var(--main-border-color); + background-color: var(--left-pane-background-color) } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index d537c5802..e7179defa 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -1,9 +1,12 @@ +import Breadcrumb from "../Breadcrumb"; import "./StatusBar.css"; export default function StatusBar() { return (
- Status bar goes here. +
+ +
); } From 4e1188484dfaf272595b1e780fbac669b0252fc4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 00:24:15 +0200 Subject: [PATCH 04/52] refactor(layout/status_bar): move breadcrumbs into layout dir --- apps/client/src/layouts/desktop_layout.tsx | 2 +- .../src/widgets/{ => layout}/Breadcrumb.css | 0 .../src/widgets/{ => layout}/Breadcrumb.tsx | 24 +++++++++---------- apps/client/src/widgets/layout/StatusBar.tsx | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) rename apps/client/src/widgets/{ => layout}/Breadcrumb.css (100%) rename apps/client/src/widgets/{ => layout}/Breadcrumb.tsx (93%) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index b25e7c70c..b18db5004 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -44,7 +44,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx"; 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 Breadcrumb from "../widgets/layout/Breadcrumb.jsx"; import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; import { isExperimentalFeatureEnabled } from "../services/experimental_features.js"; import NoteActions from "../widgets/ribbon/NoteActions.jsx"; diff --git a/apps/client/src/widgets/Breadcrumb.css b/apps/client/src/widgets/layout/Breadcrumb.css similarity index 100% rename from apps/client/src/widgets/Breadcrumb.css rename to apps/client/src/widgets/layout/Breadcrumb.css diff --git a/apps/client/src/widgets/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx similarity index 93% rename from apps/client/src/widgets/Breadcrumb.tsx rename to apps/client/src/widgets/layout/Breadcrumb.tsx index c218f7cea..8ef9be8fd 100644 --- a/apps/client/src/widgets/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -3,18 +3,18 @@ import "./Breadcrumb.css"; import { useMemo, useState } from "preact/hooks"; import { Fragment } from "preact/jsx-runtime"; -import NoteContext from "../components/note_context"; -import froca from "../services/froca"; -import ActionButton from "./react/ActionButton"; -import Dropdown from "./react/Dropdown"; -import { FormListItem } from "./react/FormList"; -import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./react/hooks"; -import Icon from "./react/Icon"; -import NoteLink from "./react/NoteLink"; -import link_context_menu from "../menus/link_context_menu"; -import { TitleEditor } from "./collections/board"; -import server from "../services/server"; -import { NoteInfoBadge } from "./BreadcrumbBadges"; +import NoteContext from "../../components/note_context"; +import froca from "../../services/froca"; +import ActionButton from "../react/ActionButton"; +import Dropdown from "../react/Dropdown"; +import { FormListItem } from "../react/FormList"; +import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "../react/hooks"; +import Icon from "../react/Icon"; +import NoteLink from "../react/NoteLink"; +import link_context_menu from "../../menus/link_context_menu"; +import { TitleEditor } from "../collections/board"; +import server from "../../services/server"; +import { NoteInfoBadge } from "../BreadcrumbBadges"; const COLLAPSE_THRESHOLD = 5; const INITIAL_ITEMS = 2; diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index e7179defa..987ad99ff 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -1,4 +1,4 @@ -import Breadcrumb from "../Breadcrumb"; +import Breadcrumb from "./Breadcrumb"; import "./StatusBar.css"; export default function StatusBar() { From df9554194a43820b286b7c76f3c192060c26fa33 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 00:34:47 +0200 Subject: [PATCH 05/52] feat(layout/status_bar): integrate language selector basically --- apps/client/src/layouts/desktop_layout.tsx | 8 +--- apps/client/src/widgets/NoteStatusBar.css | 13 ------ apps/client/src/widgets/NoteStatusBar.tsx | 25 ------------ apps/client/src/widgets/layout/StatusBar.css | 17 +++++++- apps/client/src/widgets/layout/StatusBar.tsx | 42 ++++++++++++++++++-- 5 files changed, 55 insertions(+), 50 deletions(-) delete mode 100644 apps/client/src/widgets/NoteStatusBar.css delete mode 100644 apps/client/src/widgets/NoteStatusBar.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index b18db5004..2ad525dca 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -44,7 +44,6 @@ import NoteDetail from "../widgets/NoteDetail.jsx"; 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/layout/Breadcrumb.jsx"; import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; import { isExperimentalFeatureEnabled } from "../services/experimental_features.js"; import NoteActions from "../widgets/ribbon/NoteActions.jsx"; @@ -52,7 +51,6 @@ import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; -import NoteStatusBar from "../widgets/NoteStatusBar.jsx"; import StatusBar from "../widgets/layout/StatusBar.jsx"; export default class DesktopLayout { @@ -153,11 +151,7 @@ export default class DesktopLayout { ) .optChild(!isFloatingTitlebar, titleRow) .optChild(!isNewLayout, ) - .optChild(isNewLayout, ( - - - - )) + .optChild(isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) .child() .child( diff --git a/apps/client/src/widgets/NoteStatusBar.css b/apps/client/src/widgets/NoteStatusBar.css deleted file mode 100644 index c43f13daf..000000000 --- a/apps/client/src/widgets/NoteStatusBar.css +++ /dev/null @@ -1,13 +0,0 @@ -.note-status-bar { - display: flex; - align-items: center; - padding-inline: 1em; - - .dropdown { - font-size: 0.85em; - - .dropdown-toggle { - padding: 0.1em 0.25em; - } - } -} diff --git a/apps/client/src/widgets/NoteStatusBar.tsx b/apps/client/src/widgets/NoteStatusBar.tsx deleted file mode 100644 index 3b686ff84..000000000 --- a/apps/client/src/widgets/NoteStatusBar.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import "./NoteStatusBar.css"; - -import { t } from "../services/i18n"; -import { openInAppHelpFromUrl } from "../services/utils"; -import { FormListItem } from "./react/FormList"; -import { useNoteContext } from "./react/hooks"; -import { NoteLanguageSelector } from "./ribbon/BasicPropertiesTab"; - -export default function NoteStatusBar() { - const { note } = useNoteContext(); - - return ( -
- openInAppHelpFromUrl("veGu4faJErEM")} - icon="bx bx-help-circle" - >{t("note_language.help-on-languages")} - )} - /> -
- ); -} diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 8ece8cc27..ca916e1d1 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -1,5 +1,20 @@ .component.status-bar { contain: none; border-top: 1px solid var(--main-border-color); - background-color: var(--left-pane-background-color) + background-color: var(--left-pane-background-color); + display: flex; + align-items: center; + padding-inline: 0.25em; + + > .breadcrumb-row { + flex-grow: 1; + } + + .dropdown { + font-size: 0.85em; + + .dropdown-toggle { + padding: 0.1em 0.25em; + } + } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 987ad99ff..e017728de 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -1,12 +1,46 @@ -import Breadcrumb from "./Breadcrumb"; import "./StatusBar.css"; +import FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; +import { openInAppHelpFromUrl } from "../../services/utils"; +import { FormListItem } from "../react/FormList"; +import { useNoteContext } from "../react/hooks"; +import { NoteLanguageSelector } from "../ribbon/BasicPropertiesTab"; +import Breadcrumb from "./Breadcrumb"; + +interface StatusBarContext { + note: FNote; +} + export default function StatusBar() { + const { note } = useNoteContext(); + const context = note && { note } satisfies StatusBarContext; + return (
-
- -
+ {context && <> +
+ +
+ +
+ +
+ }
); } + +function LanguageSwitcher({ note }: StatusBarContext) { + return ( + openInAppHelpFromUrl("veGu4faJErEM")} + icon="bx bx-help-circle" + >{t("note_language.help-on-languages")} + )} + /> + ); +} From 34025fa64650e4999a334cfb787fd246b6c7e136 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 14:41:10 +0200 Subject: [PATCH 06/52] fix(global_menu): dev menu wrongly positioned on horizontal layout --- apps/client/src/widgets/buttons/global_menu.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 66679dd20..fcc1cda67 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -104,10 +104,12 @@ function BrowserOnlyOptions() { } function DevelopmentOptions() { + const [ layoutOrientation ] = useTriliumOption("layoutOrientation"); + return <> Development Options - + {experimentalFeatures.map((feature) => ( ))} From 74b6e7bf6321b66ff9291d3a6473be51468ee105 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 17:55:22 +0200 Subject: [PATCH 07/52] fix(breadcrumb): some dropdowns not visible --- apps/client/src/widgets/layout/Breadcrumb.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index 8ef9be8fd..ed692adeb 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -136,7 +136,7 @@ function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePa noSelectButtonStyle buttonClassName="icon-action" hideToggleArrow - dropdownOptions={{ popperConfig: { strategy: "fixed" } }} + dropdownOptions={{ popperConfig: { strategy: "fixed", placement: "top" } }} > From 31c5323fd90db3b2145645baa738670cb46aebcf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 18:05:10 +0200 Subject: [PATCH 08/52] feat(status_bar/language): compact locale name --- apps/client/src/widgets/layout/StatusBar.tsx | 1 + .../src/widgets/ribbon/BasicPropertiesTab.tsx | 7 ++++++- .../options/components/LocaleSelector.tsx | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index e017728de..3cea9e988 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -41,6 +41,7 @@ function LanguageSwitcher({ note }: StatusBarContext) { icon="bx bx-help-circle" >{t("note_language.help-on-languages")} )} + compact /> ); } diff --git a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx index 479994ca3..dda9a7830 100644 --- a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx @@ -330,7 +330,11 @@ function NoteLanguageSwitch({ note }: { note?: FNote | null }) { ); } -export function NoteLanguageSelector({ note, extraChildren }: { note: FNote | null | undefined, extraChildren?: ComponentChildren }) { +export function NoteLanguageSelector({ note, extraChildren, ...restProps }: { + note: FNote | null | undefined, + extraChildren?: ComponentChildren, + compact?: boolean; +}) { const [ modalShown, setModalShown ] = useState(false); const [ languages ] = useTriliumOption("languages"); const DEFAULT_LOCALE = { @@ -357,6 +361,7 @@ export function NoteLanguageSelector({ note, extraChildren }: { note: FNote | nu icon="bx bx-cog" >{t("note_language.configure-languages")} } + {...restProps} /> {createPortal( , diff --git a/apps/client/src/widgets/type_widgets/options/components/LocaleSelector.tsx b/apps/client/src/widgets/type_widgets/options/components/LocaleSelector.tsx index b9b1d8758..c19fdbfac 100644 --- a/apps/client/src/widgets/type_widgets/options/components/LocaleSelector.tsx +++ b/apps/client/src/widgets/type_widgets/options/components/LocaleSelector.tsx @@ -5,13 +5,14 @@ import { useMemo } from "preact/hooks"; import Dropdown from "../../../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../../../react/FormList"; -export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren }: { +export function LocaleSelector({ id, locales, currentValue, onChange, defaultLocale, extraChildren, compact }: { id?: string; locales: Locale[], currentValue: string, onChange: (newLocale: string) => void, defaultLocale?: Locale, - extraChildren?: ComponentChildren + extraChildren?: ComponentChildren, + compact?: boolean; }) { const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue); @@ -42,7 +43,7 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc }, [ locales ]); return ( - + {processedLocales.map(locale => { if (typeof locale === "object") { return - ) + ); +} + +function getLocaleName(locale: Locale | null | undefined, compact: boolean | undefined) { + if (!locale) return ""; + if (!compact) return locale.name; + if (!locale.id) return "-"; + return locale.id.toLocaleUpperCase(); } From 4dc773c1a3d9ae093fb84067bcf80a59ad7da5d9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 18:29:40 +0200 Subject: [PATCH 09/52] refactor(status_bar/language): stop reusing UI for greater customisibility --- apps/client/src/widgets/layout/StatusBar.tsx | 47 ++++++++++++-- .../src/widgets/ribbon/BasicPropertiesTab.tsx | 55 ++++++++-------- .../options/components/LocaleSelector.tsx | 64 +++++++++---------- 3 files changed, 99 insertions(+), 67 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 3cea9e988..deb01da1c 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -3,10 +3,15 @@ import "./StatusBar.css"; import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; import { openInAppHelpFromUrl } from "../../services/utils"; -import { FormListItem } from "../react/FormList"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useNoteContext } from "../react/hooks"; -import { NoteLanguageSelector } from "../ribbon/BasicPropertiesTab"; +import { ContentLanguagesModal, NoteLanguageSelector, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; import Breadcrumb from "./Breadcrumb"; +import { useState } from "preact/hooks"; +import { createPortal } from "preact/compat"; +import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; +import Dropdown from "../react/Dropdown"; +import { Locale } from "@triliumnext/commons"; interface StatusBarContext { note: FNote; @@ -32,16 +37,44 @@ export default function StatusBar() { } function LanguageSwitcher({ note }: StatusBarContext) { + const [ modalShown, setModalShown ] = useState(false); + const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); + const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id); + return ( - + + {processedLocales.map(locale => { + if (typeof locale === "object") { + return setCurrentNoteLanguage(locale.id)} + >{locale.name} + } else { + return + } + })} + openInAppHelpFromUrl("veGu4faJErEM")} icon="bx bx-help-circle" >{t("note_language.help-on-languages")} + setModalShown(true)} + icon="bx bx-cog" + >{t("note_language.configure-languages")} + + {createPortal( + , + document.body )} - compact - /> + ); } + +export function getLocaleName(locale: Locale | null | undefined) { + if (!locale) return ""; + if (!locale.id) return "-"; + return locale.id.toLocaleUpperCase(); +} diff --git a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx index dda9a7830..761577949 100644 --- a/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/BasicPropertiesTab.tsx @@ -330,12 +330,32 @@ function NoteLanguageSwitch({ note }: { note?: FNote | null }) { ); } -export function NoteLanguageSelector({ note, extraChildren, ...restProps }: { - note: FNote | null | undefined, - extraChildren?: ComponentChildren, - compact?: boolean; -}) { +export function NoteLanguageSelector({ note }: { note: FNote | null | undefined }) { const [ modalShown, setModalShown ] = useState(false); + const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); + + return ( + <> + + setModalShown(true)} + icon="bx bx-cog" + >{t("note_language.configure-languages")} + } + /> + {createPortal( + , + document.body + )} + + ); +} + +export function useLanguageSwitcher(note: FNote | null | undefined) { const [ languages ] = useTriliumOption("languages"); const DEFAULT_LOCALE = { id: "", @@ -347,31 +367,10 @@ export function NoteLanguageSelector({ note, extraChildren, ...restProps }: { const filteredLanguages = getAvailableLocales().filter((l) => typeof l !== "object" || enabledLanguages.includes(l.id)); return filteredLanguages; }, [ languages ]); - - return ( - <> - - {extraChildren} - setModalShown(true)} - icon="bx bx-cog" - >{t("note_language.configure-languages")} - } - {...restProps} - /> - {createPortal( - , - document.body - )} - - ); + return { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage }; } -function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) { +export function ContentLanguagesModal({ modalShown, setModalShown }: { modalShown: boolean, setModalShown: (shown: boolean) => void }) { return ( void, defaultLocale?: Locale, extraChildren?: ComponentChildren, - compact?: boolean; }) { + const currentValueWithDefault = currentValue ?? defaultLocale?.id ?? ""; + const { activeLocale, processedLocales } = useProcessedLocales(locales, defaultLocale, currentValueWithDefault); + return ( + + {processedLocales.map(locale => { + if (typeof locale === "object") { + return { + onChange(locale.id); + }} + >{locale.name} + } else { + return + } + })} + {extraChildren && ( + <> + + {extraChildren} + + )} + + ); +} + +export function useProcessedLocales(locales: Locale[], defaultLocale: Locale | undefined, currentValue: string) { const activeLocale = defaultLocale?.id === currentValue ? defaultLocale : locales.find(l => l.id === currentValue); const processedLocales = useMemo(() => { @@ -36,35 +63,8 @@ export function LocaleSelector({ id, locales, currentValue, onChange, defaultLoc ]; } - if (extraChildren) { - items.push("---"); - } return items; - }, [ locales ]); + }, [ locales, defaultLocale ]); - return ( - - {processedLocales.map(locale => { - if (typeof locale === "object") { - return { - onChange(locale.id); - }} - >{locale.name} - } else { - return - } - })} - {extraChildren} - - ); -} - -function getLocaleName(locale: Locale | null | undefined, compact: boolean | undefined) { - if (!locale) return ""; - if (!compact) return locale.name; - if (!locale.id) return "-"; - return locale.id.toLocaleUpperCase(); + return { activeLocale, processedLocales }; } From 12be14e6cff8a834da6263d713227859f1f10899 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 18:46:34 +0200 Subject: [PATCH 10/52] feat(status_bar/language): display icon --- apps/client/src/widgets/layout/StatusBar.css | 20 ++++++++++++++++ apps/client/src/widgets/layout/StatusBar.tsx | 25 +++++++++++++++++--- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index ca916e1d1..42f59ed75 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -10,6 +10,26 @@ flex-grow: 1; } + > .actions-row { + padding: 0.1em; + + .status-bar-dropdown-button { + background: transparent; + padding: 0 0.5em !important; + display: flex; + align-items: center; + + &:after { + content: unset; + } + + &:focus, + &:hover { + background: var(--input-background-color); + } + } + } + .dropdown { font-size: 0.85em; diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index deb01da1c..001304798 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -10,8 +10,10 @@ import Breadcrumb from "./Breadcrumb"; import { useState } from "preact/hooks"; import { createPortal } from "preact/compat"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; -import Dropdown from "../react/Dropdown"; +import Dropdown, { DropdownProps } from "../react/Dropdown"; import { Locale } from "@triliumnext/commons"; +import clsx from "clsx"; +import Icon from "../react/Icon"; interface StatusBarContext { note: FNote; @@ -36,6 +38,23 @@ export default function StatusBar() { ); } +function StatusBarDropdown({ children, icon, text, buttonClassName, ...dropdownProps }: Omit & { + icon?: string; +}) { + return ( + + {icon && (<> )} + {text} + } + {...dropdownProps} + > + {children} + + ); +} + function LanguageSwitcher({ note }: StatusBarContext) { const [ modalShown, setModalShown ] = useState(false); const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); @@ -43,7 +62,7 @@ function LanguageSwitcher({ note }: StatusBarContext) { return ( <> - + {processedLocales.map(locale => { if (typeof locale === "object") { return setModalShown(true)} icon="bx bx-cog" >{t("note_language.configure-languages")} - + {createPortal( , document.body From c099634e39803f333283bdce3b2bb8a6c6a13777 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 18:50:48 +0200 Subject: [PATCH 11/52] feat(status_bar/language): improve display of Asian languages --- apps/client/src/widgets/layout/StatusBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 001304798..0149d2dd0 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -95,5 +95,6 @@ function LanguageSwitcher({ note }: StatusBarContext) { export function getLocaleName(locale: Locale | null | undefined) { if (!locale) return ""; if (!locale.id) return "-"; + if (locale.name.length <= 4) return locale.name; // Some locales like Japanese and Chinese look better than their ID. return locale.id.toLocaleUpperCase(); } From 24ed97f65d82cdd636a6a4f3018d589c12ff3f96 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 18:53:54 +0200 Subject: [PATCH 12/52] feat(status_bar/language): improve display of more languages --- apps/client/src/widgets/layout/StatusBar.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 0149d2dd0..0dc13df19 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -62,7 +62,7 @@ function LanguageSwitcher({ note }: StatusBarContext) { return ( <> - + {getLocaleName(activeLocale)}}> {processedLocales.map(locale => { if (typeof locale === "object") { return Date: Fri, 12 Dec 2025 18:58:54 +0200 Subject: [PATCH 13/52] feat(status_bar/language): add tooltip --- .../client/src/translations/en/translation.json | 3 +++ apps/client/src/widgets/layout/StatusBar.tsx | 17 +++++++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 136513ef8..e8ff99aab 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2156,5 +2156,8 @@ "execute_script_description": "This note is a script note. Click to execute the script.", "execute_sql": "Run SQL", "execute_sql_description": "This note is a SQL note. Click to execute the SQL query." + }, + "status_bar": { + "language_title": "Change the language of the entire content" } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 0dc13df19..b94160670 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -38,12 +38,21 @@ export default function StatusBar() { ); } -function StatusBarDropdown({ children, icon, text, buttonClassName, ...dropdownProps }: Omit & { +function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, ...dropdownProps }: Omit & { + title: string; icon?: string; }) { return ( {icon && (<> )} {text} @@ -62,7 +71,11 @@ function LanguageSwitcher({ note }: StatusBarContext) { return ( <> - {getLocaleName(activeLocale)}}> + {getLocaleName(activeLocale)}} + > {processedLocales.map(locale => { if (typeof locale === "object") { return Date: Fri, 12 Dec 2025 19:31:00 +0200 Subject: [PATCH 14/52] feat(status_bar): integrate note info badge --- .../src/translations/en/translation.json | 3 +- apps/client/src/widgets/BreadcrumbBadges.css | 24 -------- apps/client/src/widgets/BreadcrumbBadges.tsx | 32 ----------- apps/client/src/widgets/layout/Breadcrumb.tsx | 2 - apps/client/src/widgets/layout/StatusBar.css | 28 ++++++++++ apps/client/src/widgets/layout/StatusBar.tsx | 55 ++++++++++++++++--- 6 files changed, 77 insertions(+), 67 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index e8ff99aab..d50f1eeda 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2158,6 +2158,7 @@ "execute_sql_description": "This note is a SQL note. Click to execute the SQL query." }, "status_bar": { - "language_title": "Change the language of the entire content" + "language_title": "Change the language of the entire content", + "note_info_title": "View information about this note such as the creation/modification date or the note size." } } diff --git a/apps/client/src/widgets/BreadcrumbBadges.css b/apps/client/src/widgets/BreadcrumbBadges.css index 758678b01..582b9c7ba 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.css +++ b/apps/client/src/widgets/BreadcrumbBadges.css @@ -61,30 +61,6 @@ min-width: 500px; } - &.dropdown-note-info-badge { - .dropdown-menu.show ul { - list-style-type: none; - padding: 0.5em; - margin: 0; - display: table; - - li { - display: table-row; - - > strong { - display: table-cell; - padding: 0.2em 0; - } - - > span { - display: table-cell; - user-select: text; - padding-left: 2em; - } - } - } - } - .breadcrumb-badge { border-radius: 0; } diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/BreadcrumbBadges.tsx index a7ccbae9f..0ab1d7b85 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/BreadcrumbBadges.tsx @@ -5,14 +5,11 @@ import { ComponentChildren, MouseEventHandler } from "preact"; import { useRef } from "preact/hooks"; import { t } from "../services/i18n"; -import { formatDateTime } from "../utils/formatters"; import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "./react/Dropdown"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks"; import Icon from "./react/Icon"; -import { NoteSizeWidget, useNoteMetadata } from "./ribbon/NoteInfoTab"; import { useShareInfo } from "./shared_info"; -import FNote from "../entities/fnote"; export default function BreadcrumbBadges() { return ( @@ -26,35 +23,6 @@ export default function BreadcrumbBadges() { ); } -export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) { - const { metadata, ...sizeProps } = useNoteMetadata(note); - - return (note && - -
    - - - {note.type} {note.mime && ({note.mime})}} /> - {note.noteId}} /> - } /> -
-
- ); -} - -function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) { - return ( -
  • - {text}{": "} - {value} -
  • - ); -} - function ReadOnlyBadge() { const { note, noteContext } = useNoteContext(); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index ed692adeb..cc02657e0 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -14,7 +14,6 @@ import NoteLink from "../react/NoteLink"; import link_context_menu from "../../menus/link_context_menu"; import { TitleEditor } from "../collections/board"; import server from "../../services/server"; -import { NoteInfoBadge } from "../BreadcrumbBadges"; const COLLAPSE_THRESHOLD = 5; const INITIAL_ITEMS = 2; @@ -122,7 +121,6 @@ function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { inde if (index === notePathLength - 1) { return <> - ; } diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 42f59ed75..5b7d642b7 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -12,6 +12,8 @@ > .actions-row { padding: 0.1em; + display: flex; + gap: 0.1em; .status-bar-dropdown-button { background: transparent; @@ -37,4 +39,30 @@ padding: 0.1em 0.25em; } } + + .dropdown-note-info { + width: max-content; + + ul { + list-style-type: none; + padding: 0.5em; + margin: 0; + display: table; + + li { + display: table-row; + + > strong { + display: table-cell; + padding: 0.2em 0; + } + + > span { + display: table-cell; + user-select: text; + padding-left: 2em; + } + } + } + } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index b94160670..8dc261864 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -1,19 +1,23 @@ import "./StatusBar.css"; +import { Locale } from "@triliumnext/commons"; +import clsx from "clsx"; +import { createPortal } from "preact/compat"; +import { useState } from "preact/hooks"; + import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; import { openInAppHelpFromUrl } from "../../services/utils"; +import { formatDateTime } from "../../utils/formatters"; +import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useNoteContext } from "../react/hooks"; -import { ContentLanguagesModal, NoteLanguageSelector, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; -import Breadcrumb from "./Breadcrumb"; -import { useState } from "preact/hooks"; -import { createPortal } from "preact/compat"; -import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; -import Dropdown, { DropdownProps } from "../react/Dropdown"; -import { Locale } from "@triliumnext/commons"; -import clsx from "clsx"; import Icon from "../react/Icon"; +import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; +import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; +import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; +import Breadcrumb from "./Breadcrumb"; + interface StatusBarContext { note: FNote; @@ -31,6 +35,7 @@ export default function StatusBar() {
    +
    } @@ -64,6 +69,7 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions ); } +//#region Language Switcher function LanguageSwitcher({ note }: StatusBarContext) { const [ modalShown, setModalShown ] = useState(false); const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note); @@ -113,3 +119,36 @@ export function getLocaleName(locale: Locale | null | undefined) { .replace("_", "-") .toLocaleUpperCase(); } +//#endregion + +//#region Note info +export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) { + const { metadata, ...sizeProps } = useNoteMetadata(note); + + return (note && + +
      + + + {note.type} {note.mime && ({note.mime})}} /> + {note.noteId}} /> + } /> +
    +
    + ); +} + +function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) { + return ( +
  • + {text}{": "} + {value} +
  • + ); +} +//#endregion From d2b32ff5af99d482fd693b2df3996176ae90deca Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 19:47:47 +0200 Subject: [PATCH 15/52] feat(status_bar): relocate to outside split area --- apps/client/src/layouts/desktop_layout.tsx | 4 +++- apps/client/src/widgets/layout/StatusBar.css | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 2ad525dca..ac4590fec 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -177,10 +177,10 @@ export default class DesktopLayout { ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC ...this.customWidgets.get("note-detail-pane") ) - .optChild(isNewLayout, ) ) ) .child(...this.customWidgets.get("center-pane")) + ) .child( new RightPaneContainer() @@ -189,8 +189,10 @@ export default class DesktopLayout { .child(...this.customWidgets.get("right-pane")) ) ) + .optChild(!launcherPaneIsHorizontal && isNewLayout, ) ) ) + .optChild(launcherPaneIsHorizontal && isNewLayout, ) .child() // Desktop-specific dialogs. diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 5b7d642b7..688d8904d 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -5,6 +5,7 @@ display: flex; align-items: center; padding-inline: 0.25em; + min-height: 32px; > .breadcrumb-row { flex-grow: 1; From 0545b929e193d044119bb21ee49df666fc1db1e0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:17:07 +0200 Subject: [PATCH 16/52] fix(status_bar): react to active note context --- apps/client/src/widgets/layout/Breadcrumb.tsx | 4 +- apps/client/src/widgets/layout/StatusBar.tsx | 12 ++-- apps/client/src/widgets/react/hooks.tsx | 60 ++++++++++++++++++- 3 files changed, 68 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/layout/Breadcrumb.tsx b/apps/client/src/widgets/layout/Breadcrumb.tsx index cc02657e0..fe6d59b26 100644 --- a/apps/client/src/widgets/layout/Breadcrumb.tsx +++ b/apps/client/src/widgets/layout/Breadcrumb.tsx @@ -14,13 +14,13 @@ import NoteLink from "../react/NoteLink"; import link_context_menu from "../../menus/link_context_menu"; import { TitleEditor } from "../collections/board"; import server from "../../services/server"; +import FNote from "../../entities/fnote"; const COLLAPSE_THRESHOLD = 5; const INITIAL_ITEMS = 2; const FINAL_ITEMS = 2; -export default function Breadcrumb() { - const { note, noteContext } = useNoteContext(); +export default function Breadcrumb({ note, noteContext }: { note: FNote, noteContext: NoteContext }) { const notePath = buildNotePaths(noteContext?.notePathArray); return ( diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 8dc261864..459761b7b 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -2,6 +2,7 @@ import "./StatusBar.css"; import { Locale } from "@triliumnext/commons"; import clsx from "clsx"; +import { type ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; import { useState } from "preact/hooks"; @@ -11,27 +12,28 @@ import { openInAppHelpFromUrl } from "../../services/utils"; import { formatDateTime } from "../../utils/formatters"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useNoteContext } from "../react/hooks"; +import { useActiveNoteContext } from "../react/hooks"; import Icon from "../react/Icon"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; - +import NoteContext from "../../components/note_context"; interface StatusBarContext { note: FNote; + noteContext: NoteContext; } export default function StatusBar() { - const { note } = useNoteContext(); - const context = note && { note } satisfies StatusBarContext; + const { note, noteContext } = useActiveNoteContext(); + const context = note && noteContext && { note, noteContext } satisfies StatusBarContext; return (
    {context && <>
    - +
    diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 65448eb92..f6f5123f9 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -316,7 +316,7 @@ export function useNoteContext() { useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`); return { - note: note, + note, noteId: noteContext?.note?.noteId, notePath: noteContext?.notePath, hoistedNoteId: noteContext?.hoistedNoteId, @@ -327,7 +327,65 @@ export function useNoteContext() { parentComponent, isReadOnlyTemporarilyDisabled }; +} +/** + * Similar to {@link useNoteContext}, but instead of using the note context from the split container that the component is part of, it uses the active note context instead + * (the note currently focused by the user). + */ +export function useActiveNoteContext() { + const [ noteContext, setNoteContext ] = useState(appContext.tabManager.getActiveContext() ?? undefined); + const [ notePath, setNotePath ] = useState(); + const [ note, setNote ] = useState(); + const [ , setViewScope ] = useState(); + const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState(noteContext?.viewScope?.isReadOnly); + const [ refreshCounter, setRefreshCounter ] = useState(0); + + useEffect(() => { + if (!noteContext) { + setNoteContext(appContext.tabManager.getActiveContext() ?? undefined); + } + }, [ noteContext ]); + + useEffect(() => { + setNote(noteContext?.note); + }, [ notePath ]); + + useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({}) => { + const noteContext = appContext.tabManager.getActiveContext() ?? undefined; + setNoteContext(noteContext); + setNotePath(noteContext?.notePath); + setViewScope(noteContext?.viewScope); + }); + useTriliumEvent("frocaReloaded", () => { + setNote(noteContext?.note); + }); + useTriliumEvent("noteTypeMimeChanged", ({ noteId }) => { + if (noteId === note?.noteId) { + setRefreshCounter(refreshCounter + 1); + } + }); + useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { + if (eventNoteContext.ntxId === noteContext?.ntxId) { + setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled); + } + }); + + const parentComponent = useContext(ParentComponent) as ReactWrappedWidget; + useDebugValue(() => `notePath=${notePath}, ntxId=${noteContext?.ntxId}`); + + return { + note, + noteId: noteContext?.note?.noteId, + notePath: noteContext?.notePath, + hoistedNoteId: noteContext?.hoistedNoteId, + ntxId: noteContext?.ntxId, + viewScope: noteContext?.viewScope, + componentId: parentComponent.componentId, + noteContext, + parentComponent, + isReadOnlyTemporarilyDisabled + }; } /** From 0162b9d4410865f161634a7a1104573a02f95c74 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:18:50 +0200 Subject: [PATCH 17/52] fix(status_bar): language selector appearing for non-text notes --- apps/client/src/widgets/layout/StatusBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 459761b7b..03ad753c8 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -79,7 +79,7 @@ function LanguageSwitcher({ note }: StatusBarContext) { return ( <> - {getLocaleName(activeLocale)}} @@ -104,7 +104,7 @@ function LanguageSwitcher({ note }: StatusBarContext) { onClick={() => setModalShown(true)} icon="bx bx-cog" >{t("note_language.configure-languages")} - + } {createPortal( , document.body From 28e9abc8bb8b5601bf11fb5102d3c9c88a60b121 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:19:37 +0200 Subject: [PATCH 18/52] chore(status_bar): re-order icons to avoid layout shifting --- apps/client/src/widgets/layout/StatusBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 03ad753c8..39c4c01f2 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -37,8 +37,8 @@ export default function StatusBar() {
    - +
    }
    From 2b195155ed9e93bf432e4920531c33f09790fdb3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:21:55 +0200 Subject: [PATCH 19/52] fix(note_details): appearing in options --- apps/client/src/widgets/NoteTitleDetails.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/NoteTitleDetails.tsx index 60618ecb5..bbd00364c 100644 --- a/apps/client/src/widgets/NoteTitleDetails.tsx +++ b/apps/client/src/widgets/NoteTitleDetails.tsx @@ -3,11 +3,12 @@ import { useNoteContext, useNoteProperty } from "./react/hooks"; export default function NoteTitleDetails() { const { note } = useNoteContext(); + const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const noteType = useNoteProperty(note, "type"); return (
    - {note && noteType === "book" && } + {note && !isHiddenNote && noteType === "book" && }
    ); } From 95d2160c76c4b5902c0e917f30880cad082081af Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:30:15 +0200 Subject: [PATCH 20/52] feat(status_bar): integrate backlinks --- .../src/translations/en/translation.json | 9 +++---- apps/client/src/widgets/BreadcrumbBadges.css | 1 - apps/client/src/widgets/BreadcrumbBadges.tsx | 20 -------------- apps/client/src/widgets/layout/StatusBar.tsx | 27 ++++++++++++++++--- 4 files changed, 28 insertions(+), 29 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index d50f1eeda..283711567 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2146,10 +2146,6 @@ "shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.", "shared_locally": "Shared locally", "shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.", - "backlinks_one": "{{count}} backlink", - "backlinks_other": "{{count}} backlinks", - "backlinks_description_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", - "backlinks_description_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.", "clipped_note": "Web clip", "clipped_note_description": "This note was originally taken from {{url}}.\n\nClick to navigate to the source webpage.", "execute_script": "Run script", @@ -2159,6 +2155,9 @@ }, "status_bar": { "language_title": "Change the language of the entire content", - "note_info_title": "View information about this note such as the creation/modification date or the note size." + "note_info_title": "View information about this note such as the creation/modification date or the note size.", + "backlinks": "{{count}}", + "backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", + "backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks." } } diff --git a/apps/client/src/widgets/BreadcrumbBadges.css b/apps/client/src/widgets/BreadcrumbBadges.css index 582b9c7ba..55737ae9b 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.css +++ b/apps/client/src/widgets/BreadcrumbBadges.css @@ -34,7 +34,6 @@ &.read-only-badge { --color: #e33f3b; } &.share-badge { --color: #3b82f6; } &.clipped-note-badge { --color: #57a2a5; } - &.backlinks-badge { color: var(--badge-text-color); } &.execute-badge { --color: #f59e0b; } a { diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/BreadcrumbBadges.tsx index 0ab1d7b85..805e9da7b 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/BreadcrumbBadges.tsx @@ -5,7 +5,6 @@ import { ComponentChildren, MouseEventHandler } from "preact"; import { useRef } from "preact/hooks"; import { t } from "../services/i18n"; -import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "./react/Dropdown"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks"; import Icon from "./react/Icon"; @@ -16,7 +15,6 @@ export default function BreadcrumbBadges() {
    -
    @@ -66,24 +64,6 @@ function ShareBadge() { ); } -function BacklinksBadge() { - const { note, viewScope } = useNoteContext(); - const count = useBacklinkCount(note, viewScope?.viewMode === "default"); - return (note && count > 0 && - - - - ); -} - function ClippedNoteBadge() { const { note } = useNoteContext(); const [ pageUrl ] = useNoteLabel(note, "pageUrl"); diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 39c4c01f2..6279629a4 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -6,10 +6,13 @@ import { type ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; import { useState } from "preact/hooks"; +import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; +import { ViewScope } from "../../services/link"; import { openInAppHelpFromUrl } from "../../services/utils"; import { formatDateTime } from "../../utils/formatters"; +import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useActiveNoteContext } from "../react/hooks"; @@ -18,16 +21,16 @@ import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPrope import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; -import NoteContext from "../../components/note_context"; interface StatusBarContext { note: FNote; noteContext: NoteContext; + viewScope: ViewScope; } export default function StatusBar() { - const { note, noteContext } = useActiveNoteContext(); - const context = note && noteContext && { note, noteContext } satisfies StatusBarContext; + const { note, noteContext, viewScope } = useActiveNoteContext(); + const context = note && noteContext && { note, noteContext, viewScope } satisfies StatusBarContext; return (
    @@ -37,6 +40,7 @@ export default function StatusBar() {
    +
    @@ -154,3 +158,20 @@ function NoteInfoValue({ text, title, value }: { text: string; title?: string, v ); } //#endregion + +//#region Backlinks +function BacklinksBadge({ note, viewScope }: StatusBarContext) { + const count = useBacklinkCount(note, viewScope?.viewMode === "default"); + return (note && count > 0 && + + + + ); +} +//#endregion From 6eff62f73f0f2d2626c609e5fe67254fa70fd156 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:55:54 +0200 Subject: [PATCH 21/52] feat(status_bar): add new attachment count --- .../src/translations/en/translation.json | 5 +- apps/client/src/widgets/layout/StatusBar.css | 18 +++--- apps/client/src/widgets/layout/StatusBar.tsx | 58 ++++++++++++++++++- .../src/widgets/type_widgets/Attachment.tsx | 35 ++++++----- 4 files changed, 89 insertions(+), 27 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 283711567..b168d3019 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2156,8 +2156,9 @@ "status_bar": { "language_title": "Change the language of the entire content", "note_info_title": "View information about this note such as the creation/modification date or the note size.", - "backlinks": "{{count}}", "backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", - "backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks." + "backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.", + "attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.", + "attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab." } } diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 688d8904d..094e2d3b8 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -15,27 +15,29 @@ padding: 0.1em; display: flex; gap: 0.1em; + font-size: 0.85em; - .status-bar-dropdown-button { - background: transparent; + .btn { padding: 0 0.5em !important; + background: transparent; display: flex; align-items: center; - - &:after { - content: unset; - } + border: 0; &:focus, &:hover { background: var(--input-background-color); } } + + .status-bar-dropdown-button { + &:after { + content: unset; + } + } } .dropdown { - font-size: 0.85em; - .dropdown-toggle { padding: 0.1em 0.25em; } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 6279629a4..9be622371 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -4,7 +4,7 @@ import { Locale } from "@triliumnext/commons"; import clsx from "clsx"; import { type ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; -import { useState } from "preact/hooks"; +import { useContext, useRef, useState } from "preact/hooks"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; @@ -15,12 +15,17 @@ import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useActiveNoteContext } from "../react/hooks"; +import { useActiveNoteContext, useStaticTooltip, useTooltip } from "../react/hooks"; import Icon from "../react/Icon"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; +import { useAttachments } from "../type_widgets/Attachment"; +import ActionButton from "../react/ActionButton"; +import Button from "../react/Button"; +import { CommandNames } from "../../components/app_context"; +import { ParentComponent } from "../react/react_utils"; interface StatusBarContext { note: FNote; @@ -40,6 +45,7 @@ export default function StatusBar() {
    + @@ -75,6 +81,35 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions ); } +function StatusBarButton({ className, icon, text, title, triggerCommand }: { + className?: string; + icon: string; + title: string; + text?: string | number; + disabled?: boolean; + triggerCommand: CommandNames; +}) { + const parentComponent = useContext(ParentComponent); + const buttonRef = useRef(null); + useStaticTooltip(buttonRef, { + placement: "top", + fallbackPlacements: [ "top" ], + popperConfig: { strategy: "fixed" }, + title + }); + + return ( + + ); +} + //#region Language Switcher function LanguageSwitcher({ note }: StatusBarContext) { const [ modalShown, setModalShown ] = useState(false); @@ -166,7 +201,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) { @@ -175,3 +210,20 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) { ); } //#endregion + +//#region Attachment count +function AttachmentCount({ note }: StatusBarContext) { + const attachments = useAttachments(note); + const count = attachments.length; + + return (note && count > 0 && + + ); +} +//#endregion diff --git a/apps/client/src/widgets/type_widgets/Attachment.tsx b/apps/client/src/widgets/type_widgets/Attachment.tsx index 3fdc60e93..bef480c6f 100644 --- a/apps/client/src/widgets/type_widgets/Attachment.tsx +++ b/apps/client/src/widgets/type_widgets/Attachment.tsx @@ -26,24 +26,13 @@ import ws from "../../services/ws"; import appContext from "../../components/app_context"; import { ConvertAttachmentToNoteResponse } from "@triliumnext/commons"; import options from "../../services/options"; +import FNote from "../../entities/fnote"; /** * Displays the full list of attachments of a note and allows the user to interact with them. */ export function AttachmentList({ note }: TypeWidgetProps) { - const [ attachments, setAttachments ] = useState([]); - - function refresh() { - note.getAttachments().then(attachments => setAttachments(Array.from(attachments))); - } - - useEffect(refresh, [ note ]); - - useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) { - refresh(); - } - }); + const attachments = useAttachments(note); return ( <> @@ -59,7 +48,25 @@ export function AttachmentList({ note }: TypeWidgetProps) { )}
    - ) + ); +} + +export function useAttachments(note: FNote) { + const [ attachments, setAttachments ] = useState([]); + + function refresh() { + note.getAttachments().then(attachments => setAttachments(Array.from(attachments))); + } + + useEffect(refresh, [ note ]); + + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttachmentRows().some((att) => att.attachmentId && att.ownerId === note.noteId)) { + refresh(); + } + }); + + return attachments; } function AttachmentListHeader({ noteId }: { noteId: string }) { From 1b725175c6e831638cb4bc1cffa1d9503b048f02 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 20:59:57 +0200 Subject: [PATCH 22/52] refactor(status_bar): solve warnings --- apps/client/src/widgets/layout/StatusBar.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 9be622371..b28671b2c 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -6,6 +6,7 @@ import { type ComponentChildren } from "preact"; import { createPortal } from "preact/compat"; import { useContext, useRef, useState } from "preact/hooks"; +import { CommandNames } from "../../components/app_context"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; import { t } from "../../services/i18n"; @@ -15,22 +16,19 @@ import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useActiveNoteContext, useStaticTooltip, useTooltip } from "../react/hooks"; +import { useActiveNoteContext, useStaticTooltip } from "../react/hooks"; import Icon from "../react/Icon"; +import { ParentComponent } from "../react/react_utils"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; +import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; -import { useAttachments } from "../type_widgets/Attachment"; -import ActionButton from "../react/ActionButton"; -import Button from "../react/Button"; -import { CommandNames } from "../../components/app_context"; -import { ParentComponent } from "../react/react_utils"; interface StatusBarContext { note: FNote; noteContext: NoteContext; - viewScope: ViewScope; + viewScope?: ViewScope; } export default function StatusBar() { @@ -123,15 +121,16 @@ function LanguageSwitcher({ note }: StatusBarContext) { title={t("status_bar.language_title")} text={{getLocaleName(activeLocale)}} > - {processedLocales.map(locale => { + {processedLocales.map((locale, index) => { if (typeof locale === "object") { return setCurrentNoteLanguage(locale.id)} - >{locale.name} + >{locale.name}
    ; } else { - return + return ; } })} From efff38b11665033da8b5d9cc3822413a5d328998 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:19:16 +0200 Subject: [PATCH 23/52] feat(status_bar): attribute button (not yet interactive) --- .../src/translations/en/translation.json | 5 +++- apps/client/src/widgets/layout/StatusBar.tsx | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index b168d3019..c86af5e0f 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2159,6 +2159,9 @@ "backlinks_title_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", "backlinks_title_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks.", "attachments_title_one": "This note has {{count}} attachment. Click to open the list of attachments in a new tab.", - "attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab." + "attachments_title_other": "This note has {{count}} attachments. Click to open the list of attachments in a new tab.", + "attributes_one": "{{count}} attribute", + "attributes_other": "{{count}} attributes", + "attributes_title": "Click to open a dedicated pane to edit this note's owned attributes, as well as to see the list of inherited attributes." } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index b28671b2c..6696cea3e 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -16,7 +16,7 @@ import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import Dropdown, { DropdownProps } from "../react/Dropdown"; import { FormDropdownDivider, FormListItem } from "../react/FormList"; -import { useActiveNoteContext, useStaticTooltip } from "../react/hooks"; +import { useActiveNoteContext, useStaticTooltip, useTriliumEvent } from "../react/hooks"; import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; @@ -24,6 +24,7 @@ import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; +import attributes from "../../services/attributes"; interface StatusBarContext { note: FNote; @@ -43,6 +44,7 @@ export default function StatusBar() {
    + @@ -217,7 +219,7 @@ function AttachmentCount({ note }: StatusBarContext) { return (note && count > 0 && { + if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) { + setCount(note.attributes.length); + } + })); + + return ( + + ); +} +//#endregion From c6d97e3d4bcbb364b3fb7bab02881994c490fc0b Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:29:40 +0200 Subject: [PATCH 24/52] feat(status_bar): basic integration of attribute editor --- apps/client/src/widgets/layout/StatusBar.css | 112 +++++++++--------- apps/client/src/widgets/layout/StatusBar.tsx | 69 ++++++++--- .../src/widgets/ribbon/OwnedAttributesTab.tsx | 3 +- 3 files changed, 112 insertions(+), 72 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 094e2d3b8..e5ee87a08 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -2,70 +2,74 @@ contain: none; border-top: 1px solid var(--main-border-color); background-color: var(--left-pane-background-color); - display: flex; - align-items: center; - padding-inline: 0.25em; - min-height: 32px; - > .breadcrumb-row { - flex-grow: 1; - } - - > .actions-row { - padding: 0.1em; + .status-bar-main-row { + min-height: 32px; display: flex; - gap: 0.1em; - font-size: 0.85em; + align-items: center; + padding-inline: 0.25em; - .btn { - padding: 0 0.5em !important; - background: transparent; + > .breadcrumb-row { + flex-grow: 1; + } + + > .actions-row { + padding: 0.1em; display: flex; - align-items: center; - border: 0; + gap: 0.1em; + font-size: 0.85em; - &:focus, - &:hover { - background: var(--input-background-color); - } - } + .btn { + padding: 0 0.5em !important; + background: transparent; + display: flex; + align-items: center; + border: 0; - .status-bar-dropdown-button { - &:after { - content: unset; - } - } - } - - .dropdown { - .dropdown-toggle { - padding: 0.1em 0.25em; - } - } - - .dropdown-note-info { - width: max-content; - - ul { - list-style-type: none; - padding: 0.5em; - margin: 0; - display: table; - - li { - display: table-row; - - > strong { - display: table-cell; - padding: 0.2em 0; + &:focus, + &:hover { + background: var(--input-background-color); } + } - > span { - display: table-cell; - user-select: text; - padding-left: 2em; + .status-bar-dropdown-button { + &:after { + content: unset; + } + } + } + + .dropdown { + .dropdown-toggle { + padding: 0.1em 0.25em; + } + } + + .dropdown-note-info { + width: max-content; + + ul { + list-style-type: none; + padding: 0.5em; + margin: 0; + display: table; + + li { + display: table-row; + + > strong { + display: table-cell; + padding: 0.2em 0; + } + + > span { + display: table-cell; + user-select: text; + padding-left: 2em; + } } } } } + } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 6696cea3e..b3ed74b5b 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -9,6 +9,7 @@ import { useContext, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; import NoteContext from "../../components/note_context"; import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; import { openInAppHelpFromUrl } from "../../services/utils"; @@ -20,11 +21,11 @@ import { useActiveNoteContext, useStaticTooltip, useTriliumEvent } from "../reac import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; +import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; -import attributes from "../../services/attributes"; interface StatusBarContext { note: FNote; @@ -38,19 +39,23 @@ export default function StatusBar() { return (
    - {context && <> -
    - -
    + {context && } -
    - - - - - -
    - } +
    + {context && <> +
    + +
    + +
    + + + + + +
    + } +
    ); } @@ -81,14 +86,18 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions ); } -function StatusBarButton({ className, icon, text, title, triggerCommand }: { +interface StatusBarButtonBaseProps { className?: string; icon: string; title: string; text?: string | number; disabled?: boolean; - triggerCommand: CommandNames; -}) { +} + +type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; }; +type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; }; + +function StatusBarButton({ className, icon, text, title, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) { const parentComponent = useContext(ParentComponent); const buttonRef = useRef(null); useStaticTooltip(buttonRef, { @@ -103,7 +112,13 @@ function StatusBarButton({ className, icon, text, title, triggerCommand }: { ref={buttonRef} className={clsx("btn select-button", className)} type="button" - onClick={() => parentComponent?.triggerCommand(triggerCommand)} + onClick={() => { + if ("triggerCommand" in restProps) { + parentComponent?.triggerCommand(restProps.triggerCommand); + } else { + restProps.onClick(); + } + }} >  {text} @@ -246,7 +261,27 @@ function AttributesButton({ note }: StatusBarContext) { icon="bx bx-list-check" title={t("status_bar.attributes_title")} text={t("status_bar.attributes", { count })} + onClick={() => { + alert("Hi"); + }} /> ); } + +function AttributesPane({ note, noteContext }: StatusBarContext) { + const parentComponent = useContext(ParentComponent); + const api = useRef(null); + + return ( +
    + {parentComponent &&
    + ); +} //#endregion diff --git a/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx b/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx index ae3d90c07..b80d748b5 100644 --- a/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx +++ b/apps/client/src/widgets/ribbon/OwnedAttributesTab.tsx @@ -1,4 +1,5 @@ import { useMemo, useRef } from "preact/hooks"; + import { useLegacyImperativeHandlers, useTriliumEvents } from "../react/hooks"; import AttributeEditor, { AttributeEditorImperativeHandlers } from "./components/AttributeEditor"; import { TabContext } from "./ribbon-interface"; @@ -25,5 +26,5 @@ export default function OwnedAttributesTab({ note, hidden, activate, ntxId, ...r
    - ) + ); } From 870499bc3a05f7fa5c027fc3143abfa5d9fa5cf0 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:41:05 +0200 Subject: [PATCH 25/52] feat(status_bar): basic integration of inherited attributes --- apps/client/src/widgets/layout/StatusBar.css | 6 +++++- apps/client/src/widgets/layout/StatusBar.tsx | 19 +++++++++++++------ .../widgets/ribbon/InheritedAttributesTab.tsx | 6 +++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index e5ee87a08..77475869b 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -3,7 +3,7 @@ border-top: 1px solid var(--main-border-color); background-color: var(--left-pane-background-color); - .status-bar-main-row { + > .status-bar-main-row { min-height: 32px; display: flex; align-items: center; @@ -72,4 +72,8 @@ } } + > .attribute-list { + font-size: 0.9em; + } + } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index b3ed74b5b..0ca29941c 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -26,6 +26,7 @@ import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; +import InheritedAttributesTab from "../ribbon/InheritedAttributesTab"; interface StatusBarContext { note: FNote; @@ -272,15 +273,21 @@ function AttributesPane({ note, noteContext }: StatusBarContext) { const parentComponent = useContext(ParentComponent); const api = useRef(null); - return ( + const context = parentComponent && { + componentId: parentComponent.componentId, + note, + hidden: !note + }; + + return (context &&
    - {parentComponent && + +
    ); } diff --git a/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx b/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx index bc6f3eb49..9e7ff4288 100644 --- a/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx +++ b/apps/client/src/widgets/ribbon/InheritedAttributesTab.tsx @@ -9,7 +9,7 @@ import RawHtml from "../react/RawHtml"; import { joinElements } from "../react/react_utils"; import AttributeDetailWidget from "../attribute_widgets/attribute_detail"; -export default function InheritedAttributesTab({ note, componentId }: TabContext) { +export default function InheritedAttributesTab({ note, componentId }: Pick) { const [ inheritedAttributes, setInheritedAttributes ] = useState(); const [ attributeDetailWidgetEl, attributeDetailWidget ] = useLegacyWidget(() => new AttributeDetailWidget()); @@ -34,7 +34,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext refresh(); } }); - + return (
    @@ -83,4 +83,4 @@ function InheritedAttribute({ attribute, onClick }: { attribute: FAttribute, onC onClick={onClick} /> ); -} \ No newline at end of file +} From 5d438a877b8992cf9030170d2d895070ce36f92f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:44:00 +0200 Subject: [PATCH 26/52] feat(status_bar): improve alignment of attribute editor --- apps/client/src/widgets/layout/StatusBar.css | 10 ++++++++++ apps/client/src/widgets/layout/StatusBar.tsx | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index 77475869b..1fd845083 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -74,6 +74,16 @@ > .attribute-list { font-size: 0.9em; + padding: 0.5em 0.75em; + + .inherited-attributes-widget > div { + padding: 0; + font-size: 0.9em; + } + + .attribute-list-editor { + padding: 0 !important; + } } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 0ca29941c..78608c0fe 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -22,11 +22,11 @@ import Icon from "../react/Icon"; import { ParentComponent } from "../react/react_utils"; import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab"; import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor"; +import InheritedAttributesTab from "../ribbon/InheritedAttributesTab"; import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; -import InheritedAttributesTab from "../ribbon/InheritedAttributesTab"; interface StatusBarContext { note: FNote; From 45927053f3dff72aaf475c28b1148e81fc048c77 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:48:11 +0200 Subject: [PATCH 27/52] fix(ribbon): links in inherited attributes not visible --- apps/client/src/widgets/ribbon/style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index b1813d65f..8ffb3e209 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -355,6 +355,10 @@ body[dir=rtl] .attribute-list-editor { max-height: 200px; overflow: auto; padding: 14px 12px 13px 12px; + + a.reference-link { + text-decoration: underline; + } } /* #endregion */ From 685109556c5f5fda3ef2e06dd8434927b5e6d30a Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:49:42 +0200 Subject: [PATCH 28/52] chore(ribbon): hide inherited & owned attributes on new layout --- apps/client/src/widgets/ribbon/RibbonDefinition.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 0be6b9147..dbb275586 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -97,15 +97,15 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ title: t("owned_attribute_list.owned_attributes"), icon: "bx bx-list-check", content: OwnedAttributesTab, - show: ({note}) => !note?.isLaunchBarConfig(), + show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), toggleCommand: "toggleRibbonTabOwnedAttributes", - stayInDom: true + stayInDom: !isNewLayout }, { title: t("inherited_attribute_list.title"), icon: "bx bx-list-plus", content: InheritedAttributesTab, - show: ({note}) => !note?.isLaunchBarConfig(), + show: ({note}) => !isNewLayout && !note?.isLaunchBarConfig(), toggleCommand: "toggleRibbonTabInheritedAttributes" }, { From 60fc34ffac4c2d503a152b7e2f21b4557c0a4ef6 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 12 Dec 2025 21:57:42 +0200 Subject: [PATCH 29/52] feat(status_bar): functional attribute toggle button --- .../src/stylesheets/theme-next/forms.css | 9 ++++--- apps/client/src/widgets/layout/StatusBar.css | 1 + apps/client/src/widgets/layout/StatusBar.tsx | 26 ++++++++++++------- 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/apps/client/src/stylesheets/theme-next/forms.css b/apps/client/src/stylesheets/theme-next/forms.css index fb53f167d..de76231be 100644 --- a/apps/client/src/stylesheets/theme-next/forms.css +++ b/apps/client/src/stylesheets/theme-next/forms.css @@ -154,7 +154,7 @@ button.btn.btn-success kbd { color: var(--button-group-active-button-text-color); } -/* +/* * Input boxes */ @@ -399,7 +399,8 @@ button.select-button.dropdown-toggle.btn:active { select:focus, select.form-select:focus, select.form-control:focus, -.select-button.dropdown-toggle.btn:focus { +.select-button.dropdown-toggle.btn:focus, +.select-button.focus-outline:focus { box-shadow: unset; outline: 3px solid var(--input-focus-outline-color); outline-offset: 0; @@ -422,7 +423,7 @@ optgroup { line-height: 40px; } -/* +/* * File input * *