From c97038fffd3794fbee67904fe8edea64a08d6b94 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:26:25 +0200 Subject: [PATCH 01/43] chore(layout): revert breadcrumb row --- apps/client/src/layouts/desktop_layout.tsx | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index ac4590fec..7eda043b0 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -82,8 +82,16 @@ export default class DesktopLayout { const titleRow = new FlexContainer("row") .class("title-row") + .cssBlock(".title-row > * { margin: 5px; }") .child() - .child(); + .child() + .optChild(isNewLayout, ) + .child() + .child() + .child() + .child() + .child() + .optChild(isNewLayout, ); const rootContainer = new RootContainer(true) .setParent(appContext) @@ -137,18 +145,6 @@ export default class DesktopLayout { .child( new SplitNoteContainer(() => new NoteWrapperWidget() - .child( - new FlexContainer("row") - .class("breadcrumb-row") - .cssBlock(".breadcrumb-row > * { margin: 5px; }") - .optChild(isNewLayout, ) - .child() - .child() - .child() - .child() - .child() - .optChild(isNewLayout, ) - ) .optChild(!isFloatingTitlebar, titleRow) .optChild(!isNewLayout, ) .optChild(isNewLayout, ) From 3549bfb3286b193166e992b6e166e3ec967fae95 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:43:32 +0200 Subject: [PATCH 02/43] feat(layout/title): collapse badges and note title while constrained in size --- apps/client/src/widgets/note_title.css | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 2395c9d23..d461d5733 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -30,6 +30,28 @@ body.desktop .note-title-widget input.note-title { } body.experimental-feature-new-layout { + .title-row { + container-type: size; + + @container (max-width: 700px) { + .note-icon-widget .note-icon { + font-size: 1.3em; + } + + .note-title-widget { + display: flex; + align-items: center; + .note-title { + font-size: 1em; + } + } + + .breadcrumb-badge .text { + display: none; + } + } + } + .title-details { max-width: var(--max-content-width); padding: 0; From fcbd1ab0b18ea357b008d6065b1f255754bfe2e4 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:44:33 +0200 Subject: [PATCH 03/43] chore(layout/title): remove spacer --- apps/client/src/layouts/desktop_layout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 7eda043b0..85db39c47 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -86,7 +86,7 @@ export default class DesktopLayout { .child() .child() .optChild(isNewLayout, ) - .child() + .optChild(!isNewLayout, ) .child() .child() .child() From 60c8f0c78b901e063b18c086d6d03a57fa1f5206 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:47:46 +0200 Subject: [PATCH 04/43] refactor(layout/title): relocate badges to layouts dir --- apps/client/src/layouts/desktop_layout.tsx | 2 +- apps/client/src/widgets/dialogs/PopupEditor.tsx | 2 +- .../src/widgets/{ => layout}/BreadcrumbBadges.css | 0 .../src/widgets/{ => layout}/BreadcrumbBadges.tsx | 10 +++++----- 4 files changed, 7 insertions(+), 7 deletions(-) rename apps/client/src/widgets/{ => layout}/BreadcrumbBadges.css (100%) rename apps/client/src/widgets/{ => layout}/BreadcrumbBadges.tsx (95%) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 85db39c47..bf9d911f4 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -49,7 +49,7 @@ import { isExperimentalFeatureEnabled } from "../services/experimental_features. import NoteActions from "../widgets/ribbon/NoteActions.jsx"; import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; -import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx"; +import BreadcrumbBadges from "../widgets/layout/BreadcrumbBadges.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; import StatusBar from "../widgets/layout/StatusBar.jsx"; diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index 152afaa4a..7bb469c99 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -23,7 +23,7 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar"; import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter"; import FormattingToolbar from "../ribbon/FormattingToolbar"; import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar"; -import BreadcrumbBadges from "../BreadcrumbBadges"; +import BreadcrumbBadges from "../layout/BreadcrumbBadges"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); diff --git a/apps/client/src/widgets/BreadcrumbBadges.css b/apps/client/src/widgets/layout/BreadcrumbBadges.css similarity index 100% rename from apps/client/src/widgets/BreadcrumbBadges.css rename to apps/client/src/widgets/layout/BreadcrumbBadges.css diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/layout/BreadcrumbBadges.tsx similarity index 95% rename from apps/client/src/widgets/BreadcrumbBadges.tsx rename to apps/client/src/widgets/layout/BreadcrumbBadges.tsx index 805e9da7b..727b7fbf4 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/layout/BreadcrumbBadges.tsx @@ -4,11 +4,11 @@ import clsx from "clsx"; import { ComponentChildren, MouseEventHandler } from "preact"; import { useRef } from "preact/hooks"; -import { t } from "../services/i18n"; -import Dropdown, { DropdownProps } from "./react/Dropdown"; -import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "./react/hooks"; -import Icon from "./react/Icon"; -import { useShareInfo } from "./shared_info"; +import { t } from "../../services/i18n"; +import Dropdown, { DropdownProps } from "../react/Dropdown"; +import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks"; +import Icon from "../react/Icon"; +import { useShareInfo } from "../shared_info"; export default function BreadcrumbBadges() { return ( From 330d71847b5a8d03e11e8d061c5814a52b7250d2 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:54:19 +0200 Subject: [PATCH 05/43] refactor(layout/title): rename to note badges --- apps/client/src/layouts/desktop_layout.tsx | 4 ++-- apps/client/src/widgets/dialogs/PopupEditor.tsx | 4 ++-- .../widgets/layout/{BreadcrumbBadges.css => NoteBadges.css} | 4 ++-- .../widgets/layout/{BreadcrumbBadges.tsx => NoteBadges.tsx} | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename apps/client/src/widgets/layout/{BreadcrumbBadges.css => NoteBadges.css} (96%) rename apps/client/src/widgets/layout/{BreadcrumbBadges.tsx => NoteBadges.tsx} (97%) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index bf9d911f4..9d7bd4964 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -49,7 +49,7 @@ import { isExperimentalFeatureEnabled } from "../services/experimental_features. import NoteActions from "../widgets/ribbon/NoteActions.jsx"; import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; -import BreadcrumbBadges from "../widgets/layout/BreadcrumbBadges.jsx"; +import NoteBadges from "../widgets/layout/NoteBadges.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; import StatusBar from "../widgets/layout/StatusBar.jsx"; @@ -85,7 +85,7 @@ export default class DesktopLayout { .cssBlock(".title-row > * { margin: 5px; }") .child() .child() - .optChild(isNewLayout, ) + .optChild(isNewLayout, ) .optChild(!isNewLayout, ) .child() .child() diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index 7bb469c99..887568d37 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -23,7 +23,7 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar"; import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter"; import FormattingToolbar from "../ribbon/FormattingToolbar"; import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar"; -import BreadcrumbBadges from "../layout/BreadcrumbBadges"; +import NoteBadges from "../layout/NoteBadges"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); @@ -69,7 +69,7 @@ export default function PopupEditor() { - {isNewLayout && } + {isNewLayout && } } customTitleBarButtons={[{ iconClassName: "bx-expand-alt", diff --git a/apps/client/src/widgets/layout/BreadcrumbBadges.css b/apps/client/src/widgets/layout/NoteBadges.css similarity index 96% rename from apps/client/src/widgets/layout/BreadcrumbBadges.css rename to apps/client/src/widgets/layout/NoteBadges.css index 55737ae9b..fca26305b 100644 --- a/apps/client/src/widgets/layout/BreadcrumbBadges.css +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -1,8 +1,8 @@ -.component.breadcrumb-badges { +.component.note-badges { contain: none; } -.breadcrumb-badges { +.note-badges { display: flex; gap: 5px; min-width: 0; diff --git a/apps/client/src/widgets/layout/BreadcrumbBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx similarity index 97% rename from apps/client/src/widgets/layout/BreadcrumbBadges.tsx rename to apps/client/src/widgets/layout/NoteBadges.tsx index 727b7fbf4..51faf7183 100644 --- a/apps/client/src/widgets/layout/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -1,4 +1,4 @@ -import "./BreadcrumbBadges.css"; +import "./NoteBadges.css"; import clsx from "clsx"; import { ComponentChildren, MouseEventHandler } from "preact"; @@ -10,9 +10,9 @@ import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, u import Icon from "../react/Icon"; import { useShareInfo } from "../shared_info"; -export default function BreadcrumbBadges() { +export default function NoteBadges() { return ( -
+
From 0b45fb67641f73b56b7190badb2993047106c7cf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 10:57:34 +0200 Subject: [PATCH 06/43] feat(layout/title): hide note badges while editing title --- apps/client/src/widgets/note_title.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index d461d5733..1ece021e4 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -46,6 +46,7 @@ body.experimental-feature-new-layout { } } + .note-title-widget:focus-within + .note-badges, .breadcrumb-badge .text { display: none; } From 6463b0dcaa51ca6541ddc4c8206bf36e76ad20bb Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:08:34 +0200 Subject: [PATCH 07/43] chore(layout/inline-title): placeholder for the title --- apps/client/src/layouts/desktop_layout.tsx | 5 +++-- apps/client/src/widgets/layout/InlineTitle.css | 5 +++++ apps/client/src/widgets/layout/InlineTitle.tsx | 9 +++++++++ 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 apps/client/src/widgets/layout/InlineTitle.css create mode 100644 apps/client/src/widgets/layout/InlineTitle.tsx diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 9d7bd4964..38e9e5485 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -52,6 +52,7 @@ import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibb import NoteBadges from "../widgets/layout/NoteBadges.jsx"; import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; import StatusBar from "../widgets/layout/StatusBar.jsx"; +import InlineTitle from "../widgets/layout/InlineTitle.jsx"; export default class DesktopLayout { @@ -145,7 +146,7 @@ export default class DesktopLayout { .child( new SplitNoteContainer(() => new NoteWrapperWidget() - .optChild(!isFloatingTitlebar, titleRow) + .child(titleRow) .optChild(!isNewLayout, ) .optChild(isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) @@ -153,7 +154,7 @@ export default class DesktopLayout { .child( new ScrollingContainer() .filling() - .optChild(isFloatingTitlebar, titleRow) + .optChild(isNewLayout, ) .optChild(isNewLayout, ) .optChild(!isNewLayout, new ContentHeader() .child() diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css new file mode 100644 index 000000000..d4b52bd9e --- /dev/null +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -0,0 +1,5 @@ +.component.inline-title-row { + contain: none; + padding-block: 2em; + padding-inline-start: 24px; +} diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx new file mode 100644 index 000000000..e2db39e9f --- /dev/null +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -0,0 +1,9 @@ +import "./InlineTitle.css"; + +export default function InlineTitle() { + return ( +
+ Title goes here. +
+ ); +} From 912f90accfc680af13f294535dda4ebb749a1574 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:17:39 +0200 Subject: [PATCH 08/43] feat(layout/inline-title): collapse title for text notes --- .../client/src/widgets/layout/InlineTitle.tsx | 30 +++++++++++++++++++ apps/client/src/widgets/note_title.css | 4 +++ 2 files changed, 34 insertions(+) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index e2db39e9f..f92c0611a 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -1,9 +1,39 @@ import "./InlineTitle.css"; +import { useEffect, useState } from "preact/hooks"; + +import FNote from "../../entities/fnote"; +import { useNoteContext } from "../react/hooks"; + export default function InlineTitle() { + const { note, parentComponent } = useNoteContext(); + const [ shown, setShown ] = useState(shouldShow(note)); + + useEffect(() => { + setShown(shouldShow(note)); + }, [ note ]); + + useEffect(() => { + if (!shown) return; + + const titleRow = parentComponent.$widget[0] + .closest(".note-split") + ?.querySelector("&> .title-row"); + if (!titleRow) return; + + titleRow.classList.add("collapse"); + + return () => titleRow.classList.remove("collapse"); + }, [ shown, parentComponent ]); + return (
Title goes here.
); } + +function shouldShow(note: FNote | null | undefined) { + if (!note) return false; + return note.type === "text"; +} diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 1ece021e4..138b5040e 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -51,6 +51,10 @@ body.experimental-feature-new-layout { display: none; } } + + &.collapse { + display: none !important; + } } .title-details { From 9ab5eef984d232d4a52c90f355921364770abb9d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:26:42 +0200 Subject: [PATCH 09/43] feat(layout/inline-title): intersection observer --- .../client/src/widgets/layout/InlineTitle.css | 2 +- .../client/src/widgets/layout/InlineTitle.tsx | 20 +++++++++++++++---- apps/client/src/widgets/note_title.css | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index d4b52bd9e..57e45cf56 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -1,5 +1,5 @@ .component.inline-title-row { contain: none; - padding-block: 2em; + padding-bottom: 2em; padding-inline-start: 24px; } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index f92c0611a..a97ae5ebb 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -1,6 +1,6 @@ import "./InlineTitle.css"; -import { useEffect, useState } from "preact/hooks"; +import { useEffect, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import { useNoteContext } from "../react/hooks"; @@ -8,6 +8,7 @@ import { useNoteContext } from "../react/hooks"; export default function InlineTitle() { const { note, parentComponent } = useNoteContext(); const [ shown, setShown ] = useState(shouldShow(note)); + const containerRef= useRef(null); useEffect(() => { setShown(shouldShow(note)); @@ -21,13 +22,24 @@ export default function InlineTitle() { ?.querySelector("&> .title-row"); if (!titleRow) return; - titleRow.classList.add("collapse"); + const observer = new IntersectionObserver((entries) => { + titleRow.classList.toggle("collapse", entries[0].isIntersecting); + }); + if (containerRef.current) { + observer.observe(containerRef.current); + } - return () => titleRow.classList.remove("collapse"); + return () => { + titleRow.classList.remove("collapse"); + observer.disconnect(); + }; }, [ shown, parentComponent ]); return ( -
+
Title goes here.
); diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 138b5040e..9e92bb3cb 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -53,7 +53,7 @@ body.experimental-feature-new-layout { } &.collapse { - display: none !important; + visibility: hidden; } } From ac3d57d5da6088ea00b8c4d56f4482abe123c516 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:28:52 +0200 Subject: [PATCH 10/43] chore(layout): remove ribbon border --- apps/client/src/widgets/ribbon/style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index b314d74d1..bb0da9c4d 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -429,7 +429,7 @@ body.experimental-feature-new-layout { .ribbon-container { display: flex; flex-direction: column-reverse; - border-top: 1px solid var(--main-border-color); + border: 0; .ribbon-tab-spacer, .ribbon-tab-title, From 621ebe4396f3324e2b281df3b1d7777bde066a60 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:33:02 +0200 Subject: [PATCH 11/43] feat(layout/inline-title): title and icon --- apps/client/src/widgets/layout/InlineTitle.css | 2 ++ apps/client/src/widgets/layout/InlineTitle.tsx | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 57e45cf56..bb203d035 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -2,4 +2,6 @@ contain: none; padding-bottom: 2em; padding-inline-start: 24px; + display: flex; + align-items: center; } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index a97ae5ebb..17c26bda5 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,6 +3,8 @@ import "./InlineTitle.css"; import { useEffect, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; +import NoteIcon from "../note_icon"; +import NoteTitleWidget from "../note_title"; import { useNoteContext } from "../react/hooks"; export default function InlineTitle() { @@ -40,7 +42,8 @@ export default function InlineTitle() { ref={containerRef} className="inline-title-row" > - Title goes here. + +
); } From f686d9ecd05b2899f032bd37a2d095ffc9e30892 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:34:29 +0200 Subject: [PATCH 12/43] feat(layout/inline-title): keep header bar visible --- apps/client/src/widgets/layout/InlineTitle.css | 4 ++++ apps/client/src/widgets/note_title.css | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index bb203d035..3e96e3c82 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -4,4 +4,8 @@ padding-inline-start: 24px; display: flex; align-items: center; + + .note-icon-widget { + padding: 0; + } } diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 9e92bb3cb..98d091358 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -53,7 +53,10 @@ body.experimental-feature-new-layout { } &.collapse { - visibility: hidden; + .note-icon-widget, + .note-title-widget { + visibility: hidden; + } } } From 6fa97c845aead2587819b402bdabe6db639c2e95 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:37:56 +0200 Subject: [PATCH 13/43] fix(layout/inline-title): still visible in other note types --- apps/client/src/widgets/layout/InlineTitle.css | 7 +++++++ apps/client/src/widgets/layout/InlineTitle.tsx | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 3e96e3c82..15820cc62 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -1,10 +1,17 @@ .component.inline-title-row { contain: none; +} + +.inline-title-row { padding-bottom: 2em; padding-inline-start: 24px; display: flex; align-items: center; + &.hidden { + display: none !important; + } + .note-icon-widget { padding: 0; } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 17c26bda5..e22ef07ee 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -1,5 +1,6 @@ import "./InlineTitle.css"; +import clsx from "clsx"; import { useEffect, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; @@ -40,7 +41,7 @@ export default function InlineTitle() { return (
From e9dfec88c90492a667a1d0ccd20dcda35fe117b1 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:43:27 +0200 Subject: [PATCH 14/43] feat(layout/inline-title): bring back creation and modification date --- .../client/src/widgets/layout/InlineTitle.css | 11 +-- .../client/src/widgets/layout/InlineTitle.tsx | 69 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 15820cc62..e97dd3b45 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -1,12 +1,15 @@ -.component.inline-title-row { +.component.inline-title { contain: none; } -.inline-title-row { +.inline-title { padding-bottom: 2em; padding-inline-start: 24px; - display: flex; - align-items: center; + + & > .inline-title-row { + display: flex; + align-items: center; + } &.hidden { display: none !important; diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index e22ef07ee..b32e5e386 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -6,7 +6,12 @@ import { useEffect, useRef, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; -import { useNoteContext } from "../react/hooks"; +import { useNoteContext, useStaticTooltip } from "../react/hooks"; +import { ComponentChild } from "preact"; +import { joinElements } from "../react/react_utils"; +import { useNoteMetadata } from "../ribbon/NoteInfoTab"; +import { formatDateTime } from "../../utils/formatters"; +import { Trans } from "react-i18next"; export default function InlineTitle() { const { note, parentComponent } = useNoteContext(); @@ -41,10 +46,14 @@ export default function InlineTitle() { return (
- - +
+ + +
+ +
); } @@ -53,3 +62,55 @@ function shouldShow(note: FNote | null | undefined) { if (!note) return false; return note.type === "text"; } + +export function NoteTitleDetails() { + const { note, noteContext } = useNoteContext(); + const { metadata } = useNoteMetadata(note); + const isHiddenNote = note?.noteId.startsWith("_"); + const isDefaultView = noteContext?.viewScope?.viewMode === "default"; + + const items: ComponentChild[] = [ + (isDefaultView && !isHiddenNote && metadata?.dateCreated && + ), + (isDefaultView && !isHiddenNote && metadata?.dateModified && + ) + ].filter(item => !!item); + + return items.length && ( +
+ {joinElements(items, " • ")} +
+ ); +} + +function TextWithValue({ i18nKey, value, valueTooltip }: { + i18nKey: string; + value: string; + valueTooltip: string; +}) { + const listItemRef = useRef(null); + useStaticTooltip(listItemRef, { + selector: "span.value", + title: valueTooltip, + popperConfig: { placement: "bottom" } + }); + + return ( +
  • + {value} as React.ReactElement + }} + /> +
  • + ); +} From f040a0b6d11ea9323d68dd618264e86aabf41afe Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:46:42 +0200 Subject: [PATCH 15/43] refactor(layout/inline-title): separate old title details into title actions --- apps/client/src/layouts/desktop_layout.tsx | 4 +-- .../src/widgets/layout/NoteTitleActions.css | 26 +++++++++++++++++++ .../NoteTitleActions.tsx} | 9 ++++--- apps/client/src/widgets/note_title.css | 22 ---------------- 4 files changed, 33 insertions(+), 28 deletions(-) create mode 100644 apps/client/src/widgets/layout/NoteTitleActions.css rename apps/client/src/widgets/{NoteTitleDetails.tsx => layout/NoteTitleActions.tsx} (55%) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 38e9e5485..86b7ee48e 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -50,7 +50,7 @@ import NoteActions from "../widgets/ribbon/NoteActions.jsx"; import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; import NoteBadges from "../widgets/layout/NoteBadges.jsx"; -import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; +import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx"; import StatusBar from "../widgets/layout/StatusBar.jsx"; import InlineTitle from "../widgets/layout/InlineTitle.jsx"; @@ -155,7 +155,7 @@ export default class DesktopLayout { new ScrollingContainer() .filling() .optChild(isNewLayout, ) - .optChild(isNewLayout, ) + .optChild(isNewLayout, ) .optChild(!isNewLayout, new ContentHeader() .child() .child() diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css new file mode 100644 index 000000000..b29697d5c --- /dev/null +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -0,0 +1,26 @@ +body.experimental-feature-new-layout { + .component.title-actions { + contain: none; + } + + .title-actions { + max-width: var(--max-content-width); + padding: 0; + padding-inline-start: 24px; + padding-inline-end: 16px; + display: flex; + gap: 0.25em; + align-items: center; + + .dropdown-menu { + input.form-control { + padding: 2px 8px; + margin-left: 1em; + } + } + + .spacer { + flex-grow: 1; + } + } +} diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/layout/NoteTitleActions.tsx similarity index 55% rename from apps/client/src/widgets/NoteTitleDetails.tsx rename to apps/client/src/widgets/layout/NoteTitleActions.tsx index bbd00364c..765eefca1 100644 --- a/apps/client/src/widgets/NoteTitleDetails.tsx +++ b/apps/client/src/widgets/layout/NoteTitleActions.tsx @@ -1,13 +1,14 @@ -import CollectionProperties from "./note_bars/CollectionProperties"; -import { useNoteContext, useNoteProperty } from "./react/hooks"; +import CollectionProperties from "../note_bars/CollectionProperties"; +import { useNoteContext, useNoteProperty } from "../react/hooks"; +import "./NoteTitleActions.css"; -export default function NoteTitleDetails() { +export default function NoteTitleActions() { const { note } = useNoteContext(); const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_"); const noteType = useNoteProperty(note, "type"); return ( -
    +
    {note && !isHiddenNote && noteType === "book" && }
    ); diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 98d091358..155d64dc5 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -60,34 +60,12 @@ body.experimental-feature-new-layout { } } - .title-details { - max-width: var(--max-content-width); - padding: 0; - padding-inline-start: 24px; - } - - .title-details { - padding-inline-end: 16px; - - .dropdown-menu { - input.form-control { - padding: 2px 8px; - margin-left: 1em; - } - } - - .spacer { - flex-grow: 1; - } - } - .note-split.type-code:not(.mime-text-x-sqlite) .title-details { background-color: var(--main-background-color); } .title-details { margin-top: 0; - contain: none; display: flex; gap: 0.25em; margin: 0; From 61d3141bceed054464bb4989c2de8bb7e769a03d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:49:05 +0200 Subject: [PATCH 16/43] refactor(layout/inline-title): extract specific styles --- .../client/src/widgets/layout/InlineTitle.css | 16 ++++++++++++ .../src/widgets/layout/NoteTitleActions.css | 8 +++--- apps/client/src/widgets/note_title.css | 26 ------------------- 3 files changed, 21 insertions(+), 29 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index e97dd3b45..093f58134 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -19,3 +19,19 @@ padding: 0; } } + +body.prefers-centered-content .inline-title { + margin-inline: auto; +} + +.title-details { + margin-top: 0; + display: flex; + gap: 0.25em; + margin: 0; + list-style-type: none; + + span.value { + font-weight: 500; + } +} diff --git a/apps/client/src/widgets/layout/NoteTitleActions.css b/apps/client/src/widgets/layout/NoteTitleActions.css index b29697d5c..52aea7a90 100644 --- a/apps/client/src/widgets/layout/NoteTitleActions.css +++ b/apps/client/src/widgets/layout/NoteTitleActions.css @@ -4,13 +4,15 @@ body.experimental-feature-new-layout { } .title-actions { - max-width: var(--max-content-width); padding: 0; - padding-inline-start: 24px; - padding-inline-end: 16px; display: flex; gap: 0.25em; align-items: center; + width: 100%; + max-width: unset; + padding-inline-start: 15px; + padding-bottom: 0.2em; + font-size: 0.8em; .dropdown-menu { input.form-control { diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 155d64dc5..6b55977f3 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -64,32 +64,6 @@ body.experimental-feature-new-layout { background-color: var(--main-background-color); } - .title-details { - margin-top: 0; - display: flex; - gap: 0.25em; - margin: 0; - list-style-type: none; - - span.value { - font-weight: 500; - } - } - - .scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)), - .note-split.type-book { - .title-details { - width: 100%; - max-width: unset; - padding-inline-start: 15px; - padding-bottom: 0.2em; - font-size: 0.8em; - } - } - - &.prefers-centered-content .title-details { - margin-inline: auto; - } } body.experimental-feature-floating-titlebar { From cef14a3b19315cb5c295541fb686404e41a9d6bf Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:51:57 +0200 Subject: [PATCH 17/43] feat(layout/inline-title): support code --- apps/client/src/widgets/layout/InlineTitle.css | 4 ++++ apps/client/src/widgets/layout/InlineTitle.tsx | 13 +++++++++---- apps/client/src/widgets/note_title.css | 5 ----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 093f58134..147efceed 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -20,6 +20,10 @@ } } +.note-split.type-code:not(.mime-text-x-sqlite) .inline-title { + background-color: var(--main-background-color); +} + body.prefers-centered-content .inline-title { margin-inline: auto; } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index b32e5e386..f4c176c14 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -1,17 +1,22 @@ import "./InlineTitle.css"; +import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; +import { ComponentChild } from "preact"; import { useEffect, useRef, useState } from "preact/hooks"; +import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; +import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import { useNoteContext, useStaticTooltip } from "../react/hooks"; -import { ComponentChild } from "preact"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; -import { formatDateTime } from "../../utils/formatters"; -import { Trans } from "react-i18next"; + +const supportedNoteTypes = new Set([ + "text", "code" +]); export default function InlineTitle() { const { note, parentComponent } = useNoteContext(); @@ -60,7 +65,7 @@ export default function InlineTitle() { function shouldShow(note: FNote | null | undefined) { if (!note) return false; - return note.type === "text"; + return supportedNoteTypes.has(note.type); } export function NoteTitleDetails() { diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 6b55977f3..c928a21b1 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -59,11 +59,6 @@ body.experimental-feature-new-layout { } } } - - .note-split.type-code:not(.mime-text-x-sqlite) .title-details { - background-color: var(--main-background-color); - } - } body.experimental-feature-floating-titlebar { From 0ef90c61653e6c4bf437a0c8213f043efe5206ff Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:57:53 +0200 Subject: [PATCH 18/43] fix(layout/inline-title): hide in attachments and other view scopes --- .../client/src/widgets/layout/InlineTitle.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index f4c176c14..62e1052d6 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -7,6 +7,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; +import { ViewScope } from "../../services/link"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; @@ -19,13 +20,13 @@ const supportedNoteTypes = new Set([ ]); export default function InlineTitle() { - const { note, parentComponent } = useNoteContext(); - const [ shown, setShown ] = useState(shouldShow(note)); + const { note, parentComponent, viewScope } = useNoteContext(); + const [ shown, setShown ] = useState(shouldShow(note, viewScope)); const containerRef= useRef(null); useEffect(() => { - setShown(shouldShow(note)); - }, [ note ]); + setShown(shouldShow(note, viewScope)); + }, [ note, viewScope ]); useEffect(() => { if (!shown) return; @@ -63,25 +64,25 @@ export default function InlineTitle() { ); } -function shouldShow(note: FNote | null | undefined) { +function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undefined) { if (!note) return false; + if (viewScope?.viewMode !== "default") return false; return supportedNoteTypes.has(note.type); } export function NoteTitleDetails() { - const { note, noteContext } = useNoteContext(); + const { note } = useNoteContext(); const { metadata } = useNoteMetadata(note); const isHiddenNote = note?.noteId.startsWith("_"); - const isDefaultView = noteContext?.viewScope?.viewMode === "default"; const items: ComponentChild[] = [ - (isDefaultView && !isHiddenNote && metadata?.dateCreated && + (!isHiddenNote && metadata?.dateCreated && ), - (isDefaultView && !isHiddenNote && metadata?.dateModified && + (!isHiddenNote && metadata?.dateModified && ) ].filter(item => !!item); - return items.length && ( + return items.length > 0 && (
    {joinElements(items, " • ")}
    From 0939975631e79c41ed84a204eed6d053356be42f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 11:58:35 +0200 Subject: [PATCH 19/43] style(layout/inline-title): use muted text color --- apps/client/src/widgets/layout/InlineTitle.css | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 147efceed..b18f1066b 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -34,6 +34,7 @@ body.prefers-centered-content .inline-title { gap: 0.25em; margin: 0; list-style-type: none; + color: var(--muted-text-color); span.value { font-weight: 500; From 70c918c9c6d7e225d88355f63c5bb9d0da032520 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:01:06 +0200 Subject: [PATCH 20/43] feat(layout/inline-title): support in options as well --- apps/client/src/widgets/layout/InlineTitle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 62e1052d6..e9eb66a39 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -67,6 +67,7 @@ export default function InlineTitle() { function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undefined) { if (!note) return false; if (viewScope?.viewMode !== "default") return false; + if (note.noteId.startsWith("_options")) return true; return supportedNoteTypes.has(note.type); } From 4473f80d733197456529044ce0853841776b5b95 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:02:17 +0200 Subject: [PATCH 21/43] refactor(layout): remove floating title bar experiment --- apps/client/src/layouts/desktop_layout.tsx | 1 - .../src/services/experimental_features.ts | 5 --- .../src/translations/en/translation.json | 4 +-- apps/client/src/widgets/note_title.css | 32 ------------------- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 86b7ee48e..2aa7c030a 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -79,7 +79,6 @@ export default class DesktopLayout { const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac); const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); - const isFloatingTitlebar = isExperimentalFeatureEnabled("floating-titlebar"); const titleRow = new FlexContainer("row") .class("title-row") diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts index 6e2f14e86..398467410 100644 --- a/apps/client/src/services/experimental_features.ts +++ b/apps/client/src/services/experimental_features.ts @@ -12,11 +12,6 @@ export const experimentalFeatures = [ id: "new-layout", name: t("experimental_features.new_layout_name"), description: t("experimental_features.new_layout_description"), - }, - { - id: "floating-titlebar", - name: t("experimental_features.floating_titlebar"), - description: t("experimental_features.floating_titlebar_description"), } ] as const satisfies ExperimentalFeature[]; diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 17b1b90ae..0f5e2be95 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1102,9 +1102,7 @@ "title": "Experimental Options", "disclaimer": "These options are experimental and may cause instability. Use with caution.", "new_layout_name": "New Layout", - "new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases.", - "floating_titlebar": "Floating Titlebar", - "floating_titlebar_description": "The title bar is part of the content and is scrolled along with the note content." + "new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases." }, "fonts": { "theme_defined": "Theme defined", diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index c928a21b1..4f8176a1d 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -60,35 +60,3 @@ body.experimental-feature-new-layout { } } } - -body.experimental-feature-floating-titlebar { - .title-row { - max-width: var(--max-content-width); - padding: 0; - padding-inline-start: 24px; - } - - .note-icon-widget { - padding: 0; - width: 41px; - } - - .note-split.type-code:not(.mime-text-x-sqlite) .title-row { - background-color: var(--main-background-color); - } - - .scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)), - .note-split.type-book { - .title-row { - width: 100%; - max-width: unset; - padding-inline-start: 15px; - padding-bottom: 0.2em; - font-size: 0.8em; - } - } - - &.prefers-centered-content .title-row { - margin-inline: auto; - } -} From 50a847777e38b456cd60bc16e9a6695474bd7801 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:25:01 +0200 Subject: [PATCH 22/43] feat(layout/inline-title): basic note type switcher --- .../src/widgets/dialogs/note_type_chooser.tsx | 4 +- .../client/src/widgets/layout/InlineTitle.css | 16 ++++ .../client/src/widgets/layout/InlineTitle.tsx | 27 ++++++- apps/client/src/widgets/layout/NoteBadges.css | 65 +++------------- apps/client/src/widgets/layout/NoteBadges.tsx | 61 +-------------- apps/client/src/widgets/react/Badge.css | 49 ++++++++++++ apps/client/src/widgets/react/Badge.tsx | 78 ++++++++++++++++++- 7 files changed, 178 insertions(+), 122 deletions(-) create mode 100644 apps/client/src/widgets/react/Badge.css diff --git a/apps/client/src/widgets/dialogs/note_type_chooser.tsx b/apps/client/src/widgets/dialogs/note_type_chooser.tsx index 7db061c1a..efb44f48c 100644 --- a/apps/client/src/widgets/dialogs/note_type_chooser.tsx +++ b/apps/client/src/widgets/dialogs/note_type_chooser.tsx @@ -8,7 +8,7 @@ import note_types from "../../services/note_types"; import { MenuCommandItem, MenuItem } from "../../menus/context_menu"; import { TreeCommandNames } from "../../menus/tree_context_menu"; import { Suggestion } from "../../services/note_autocomplete"; -import Badge from "../react/Badge"; +import SimpleBadge from "../react/Badge"; import { useTriliumEvent } from "../react/hooks"; export interface ChooseNoteTypeResponse { @@ -108,7 +108,7 @@ export default function NoteTypeChooserDialogComponent() { value={[ item.type, item.templateNoteId ].join(",") } icon={item.uiIcon}> {item.title} - {item.badges && item.badges.map((badge) => )} + {item.badges && item.badges.map((badge) => )} ; } })} diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index b18f1066b..237398449 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -40,3 +40,19 @@ body.prefers-centered-content .inline-title { font-weight: 500; } } + +.note-type-switcher { + padding: 1em 0; + display: flex; + overflow-x: auto; + min-width: 0; + gap: 5px; + --badge-radius: 12px; + + .ext-badge { + --color: var(--input-background-color); + color: var(--main-text-color); + flex-shrink: 0; + font-size: 0.9rem; + } +} diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index e9eb66a39..06959a6e3 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,7 +3,7 @@ import "./InlineTitle.css"; import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; import { ComponentChild } from "preact"; -import { useEffect, useRef, useState } from "preact/hooks"; +import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; @@ -14,6 +14,9 @@ import NoteTitleWidget from "../note_title"; import { useNoteContext, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; +import { NOTE_TYPES } from "../../services/note_types"; +import { Badge } from "../react/Badge"; +import server from "../../services/server"; const supportedNoteTypes = new Set([ "text", "code" @@ -60,6 +63,7 @@ export default function InlineTitle() {
    +
    ); } @@ -71,6 +75,7 @@ function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undef return supportedNoteTypes.has(note.type); } +//#region Title details export function NoteTitleDetails() { const { note } = useNoteContext(); const { metadata } = useNoteMetadata(note); @@ -121,3 +126,23 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { ); } +//#endregion + +//#region Note type switcher +function NoteTypeSwitcher() { + const { note } = useNoteContext(); + const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); + + return (note && +
    + {noteTypes.map(noteType => ( + server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })} + /> + ))} +
    + ); +} +//#endregion diff --git a/apps/client/src/widgets/layout/NoteBadges.css b/apps/client/src/widgets/layout/NoteBadges.css index fca26305b..fa3bc504f 100644 --- a/apps/client/src/widgets/layout/NoteBadges.css +++ b/apps/client/src/widgets/layout/NoteBadges.css @@ -9,64 +9,19 @@ flex-shrink: 1; overflow: hidden; --badge-radius: 12px; -} -.breadcrumb-badge { - display: flex; - align-items: center; - padding: 2px 6px; - border-radius: var(--badge-radius); - font-size: 0.75em; - background-color: var(--color, transparent); - color: white; - min-width: 0; - flex-shrink: 1; + .ext-badge { + &.temporarily-editable-badge { --color: #4fa52b; } + &.read-only-badge { --color: #e33f3b; } + &.share-badge { --color: #3b82f6; } + &.clipped-note-badge { --color: #57a2a5; } + &.execute-badge { --color: #f59e0b; } + } - &.clickable { - cursor: pointer; - - &:hover { - background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black); + .dropdown-badge { + &.dropdown-backlinks-badge .dropdown-menu { + min-width: 500px; } } - - &.temporarily-editable-badge { --color: #4fa52b; } - &.read-only-badge { --color: #e33f3b; } - &.share-badge { --color: #3b82f6; } - &.clipped-note-badge { --color: #57a2a5; } - &.execute-badge { --color: #f59e0b; } - - a { - color: inherit !important; - text-decoration: none; - } - - > * { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } } -.breadcrumb-dropdown-badge { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - border-radius: var(--badge-radius); - - &.dropdown-backlinks-badge .dropdown-menu { - min-width: 500px; - } - - .breadcrumb-badge { - border-radius: 0; - } - - .btn { - border: 0; - margin: 0; - padding: 0; - } -} diff --git a/apps/client/src/widgets/layout/NoteBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx index 51faf7183..0959b773c 100644 --- a/apps/client/src/widgets/layout/NoteBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -9,6 +9,7 @@ import Dropdown, { DropdownProps } from "../react/Dropdown"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useStaticTooltip } from "../react/hooks"; import Icon from "../react/Icon"; import { useShareInfo } from "../shared_info"; +import { Badge } from "../react/Badge"; export default function NoteBadges() { return ( @@ -97,63 +98,3 @@ function ExecuteBadge() { /> ); } - -interface BadgeProps { - text?: string; - icon?: string; - className: string; - tooltip?: string; - onClick?: MouseEventHandler; - href?: string; -} - -function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) { - const containerRef = useRef(null); - useStaticTooltip(containerRef, { - placement: "bottom", - fallbackPlacements: [ "bottom" ], - animation: false, - html: true, - title: tooltip - }); - - const content = <> - {icon && <> } - {text} - ; - - return ( -
    - {href ? {content} : {content}} -
    - ); -} - -function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & { - children: ComponentChildren, - dropdownOptions?: Partial -}) { - return ( - } - noDropdownListStyle - noSelectButtonStyle - hideToggleArrow - title={tooltip} - titlePosition="bottom" - {...dropdownOptions} - dropdownOptions={{ - ...dropdownOptions?.dropdownOptions, - popperConfig: { - ...dropdownOptions?.dropdownOptions?.popperConfig, - placement: "bottom", strategy: "fixed" - } - }} - >{children} - ); -} diff --git a/apps/client/src/widgets/react/Badge.css b/apps/client/src/widgets/react/Badge.css new file mode 100644 index 000000000..83f3f05ef --- /dev/null +++ b/apps/client/src/widgets/react/Badge.css @@ -0,0 +1,49 @@ +.ext-badge { + display: flex; + align-items: center; + padding: 2px 6px; + border-radius: var(--badge-radius); + font-size: 0.75em; + background-color: var(--color, transparent); + color: white; + min-width: 0; + flex-shrink: 1; + + &.clickable { + cursor: pointer; + + &:hover { + background-color: color-mix(in srgb, var(--color, --badge-background-color) 80%, black); + } + } + + a { + color: inherit !important; + text-decoration: none; + } + + > * { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } +} + +.dropdown-badge { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: var(--badge-radius); + + .ext-badge { + border-radius: 0; + } + + .btn { + border: 0; + margin: 0; + padding: 0; + } +} diff --git a/apps/client/src/widgets/react/Badge.tsx b/apps/client/src/widgets/react/Badge.tsx index 49d4b879d..e7844368c 100644 --- a/apps/client/src/widgets/react/Badge.tsx +++ b/apps/client/src/widgets/react/Badge.tsx @@ -1,8 +1,78 @@ -interface BadgeProps { +import "./Badge.css"; + +import clsx from "clsx"; +import { ComponentChildren, MouseEventHandler } from "preact"; +import { useRef } from "preact/hooks"; + +import Dropdown, { DropdownProps } from "./Dropdown"; +import { useStaticTooltip } from "./hooks"; +import Icon from "./Icon"; + +interface SimpleBadgeProps { className?: string; title: string; } -export default function Badge({ title, className }: BadgeProps) { - return {title} -} \ No newline at end of file +interface BadgeProps { + text?: string; + icon?: string; + className?: string; + tooltip?: string; + onClick?: MouseEventHandler; + href?: string; +} + +export default function SimpleBadge({ title, className }: SimpleBadgeProps) { + return {title}; +} + +export function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) { + const containerRef = useRef(null); + useStaticTooltip(containerRef, { + placement: "bottom", + fallbackPlacements: [ "bottom" ], + animation: false, + html: true, + title: tooltip + }); + + const content = <> + {icon && <> } + {text} + ; + + return ( +
    + {href ? {content} : {content}} +
    + ); +} + +export function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & { + children: ComponentChildren, + dropdownOptions?: Partial +}) { + return ( + } + noDropdownListStyle + noSelectButtonStyle + hideToggleArrow + title={tooltip} + titlePosition="bottom" + {...dropdownOptions} + dropdownOptions={{ + ...dropdownOptions?.dropdownOptions, + popperConfig: { + ...dropdownOptions?.dropdownOptions?.popperConfig, + placement: "bottom", strategy: "fixed" + } + }} + >{children} + ); +} From e06abe6e5b4d1d1c67568be9ca63e65112027e9d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:26:02 +0200 Subject: [PATCH 23/43] fix(layout/inline-title): current note type displayed in switcher --- apps/client/src/widgets/layout/InlineTitle.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 06959a6e3..2fa802708 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -11,7 +11,7 @@ import { ViewScope } from "../../services/link"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; -import { useNoteContext, useStaticTooltip } from "../react/hooks"; +import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { NOTE_TYPES } from "../../services/note_types"; @@ -131,11 +131,12 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { //#region Note type switcher function NoteTypeSwitcher() { const { note } = useNoteContext(); + const currentNoteType = useNoteProperty(note, "type"); const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); return (note &&
    - {noteTypes.map(noteType => ( + {noteTypes.map(noteType => noteType.type !== currentNoteType && ( Date: Sat, 13 Dec 2025 12:27:45 +0200 Subject: [PATCH 24/43] feat(layout/inline-title): horizontal scroll via wheel --- apps/client/src/widgets/layout/InlineTitle.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 2fa802708..abf2e6c09 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -8,15 +8,16 @@ import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; import { ViewScope } from "../../services/link"; +import { NOTE_TYPES } from "../../services/note_types"; +import server from "../../services/server"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; +import { Badge } from "../react/Badge"; import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; -import { NOTE_TYPES } from "../../services/note_types"; -import { Badge } from "../react/Badge"; -import server from "../../services/server"; +import { onWheelHorizontalScroll } from "../widget_utils"; const supportedNoteTypes = new Set([ "text", "code" @@ -135,7 +136,10 @@ function NoteTypeSwitcher() { const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); return (note && -
    +
    {noteTypes.map(noteType => noteType.type !== currentNoteType && ( Date: Sat, 13 Dec 2025 12:28:22 +0200 Subject: [PATCH 25/43] feat(layout/inline-title): add icons --- apps/client/src/widgets/layout/InlineTitle.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index abf2e6c09..6dd0512a5 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -144,6 +144,7 @@ function NoteTypeSwitcher() { server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })} /> ))} From 9946d8c6b9f61a72139cf5c9a1d394597942dc0f Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:29:59 +0200 Subject: [PATCH 26/43] fix(layout/statusbar): code note switcher displayed for other note types --- 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 d18269210..07fd730d4 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -356,7 +356,7 @@ function CodeNoteSwitcher({ note }: StatusBarContext) { mimeTypes.find(m => m.mime === currentNoteMime) ), [ mimeTypes, currentNoteMime ]); - return ( + return (note.type === "code" && <> Date: Sat, 13 Dec 2025 12:43:15 +0200 Subject: [PATCH 27/43] feat(layout/inline-title): not reacting to note type changes --- apps/client/src/widgets/layout/InlineTitle.tsx | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 6dd0512a5..62bdeab90 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -25,12 +25,13 @@ const supportedNoteTypes = new Set([ export default function InlineTitle() { const { note, parentComponent, viewScope } = useNoteContext(); - const [ shown, setShown ] = useState(shouldShow(note, viewScope)); + const type = useNoteProperty(note, "type"); + const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope)); const containerRef= useRef(null); useEffect(() => { - setShown(shouldShow(note, viewScope)); - }, [ note, viewScope ]); + setShown(shouldShow(note?.noteId, type, viewScope)); + }, [ note, type, viewScope ]); useEffect(() => { if (!shown) return; @@ -69,11 +70,10 @@ export default function InlineTitle() { ); } -function shouldShow(note: FNote | null | undefined, viewScope: ViewScope | undefined) { - if (!note) return false; +function shouldShow(noteId: string | undefined, type: NoteType | undefined, viewScope: ViewScope | undefined) { if (viewScope?.viewMode !== "default") return false; - if (note.noteId.startsWith("_options")) return true; - return supportedNoteTypes.has(note.type); + if (noteId?.startsWith("_options")) return true; + return type && supportedNoteTypes.has(type); } //#region Title details From 470f6e533446c42c6581da3426b5c965c7e9f7a9 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 12:48:20 +0200 Subject: [PATCH 28/43] feat(layout/inline-title): hide note type switcher when empty --- apps/client/src/widgets/layout/InlineTitle.css | 1 + apps/client/src/widgets/layout/InlineTitle.tsx | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 237398449..bdaa1bba1 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -47,6 +47,7 @@ body.prefers-centered-content .inline-title { overflow-x: auto; min-width: 0; gap: 5px; + min-height: 60px; --badge-radius: 12px; .ext-badge { diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 62bdeab90..28ea8dfc2 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -14,7 +14,7 @@ import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import { Badge } from "../react/Badge"; -import { useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; +import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { onWheelHorizontalScroll } from "../widget_utils"; @@ -132,6 +132,7 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { //#region Note type switcher function NoteTypeSwitcher() { const { note } = useNoteContext(); + const blob = useNoteBlob(note); const currentNoteType = useNoteProperty(note, "type"); const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); @@ -140,7 +141,7 @@ function NoteTypeSwitcher() { className="note-type-switcher" onWheel={onWheelHorizontalScroll} > - {noteTypes.map(noteType => noteType.type !== currentNoteType && ( + {blob?.contentLength === 0 && noteTypes.map(noteType => noteType.type !== currentNoteType && ( Date: Sat, 13 Dec 2025 12:49:51 +0200 Subject: [PATCH 29/43] fix(layout/inline-title): hide note type switcher on other note types --- apps/client/src/widgets/layout/InlineTitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 28ea8dfc2..e802bebc4 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -136,7 +136,7 @@ function NoteTypeSwitcher() { const currentNoteType = useNoteProperty(note, "type"); const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); - return (note && + return (note?.type === "text" &&
    Date: Sat, 13 Dec 2025 12:55:24 +0200 Subject: [PATCH 30/43] feat(layout/inline-title): add an intro text --- apps/client/src/translations/en/translation.json | 3 ++- apps/client/src/widgets/layout/InlineTitle.css | 5 ++++- apps/client/src/widgets/layout/InlineTitle.tsx | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 0f5e2be95..729d41a28 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1753,7 +1753,8 @@ "note_title": { "placeholder": "type note's title here...", "created_on": "Created on ", - "last_modified": "Last modified on " + "last_modified": "Last modified on ", + "note_type_switcher_label": "Switch from {{type}} to:" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index bdaa1bba1..a0e3f6edd 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -50,10 +50,13 @@ body.prefers-centered-content .inline-title { min-height: 60px; --badge-radius: 12px; + >* { + flex-shrink: 0; + } + .ext-badge { --color: var(--input-background-color); color: var(--main-text-color); - flex-shrink: 0; font-size: 0.9rem; } } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index e802bebc4..91335ed64 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -6,7 +6,7 @@ import { ComponentChild } from "preact"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; -import FNote from "../../entities/fnote"; +import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; import { NOTE_TYPES } from "../../services/note_types"; import server from "../../services/server"; @@ -135,12 +135,14 @@ function NoteTypeSwitcher() { const blob = useNoteBlob(note); const currentNoteType = useNoteProperty(note, "type"); const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); + const currentNoteTypeData = useMemo(() => noteTypes.find(t => t.type === currentNoteType), [ noteTypes, currentNoteType ]); return (note?.type === "text" &&
    +
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    {blob?.contentLength === 0 && noteTypes.map(noteType => noteType.type !== currentNoteType && ( Date: Sat, 13 Dec 2025 12:59:32 +0200 Subject: [PATCH 31/43] feat(vscode): eslint on save --- .vscode/settings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2eaee6a3b..57d22dcb8 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -37,6 +37,9 @@ "apps/server/src/assets/doc_notes/**": true, "apps/edit-docs/demo/**": true }, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, "eslint.rules.customizations": [ { "rule": "*", "severity": "warn" } ] From 4c2fe8a8463f5412a255592a73b9317a47900278 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 13:12:03 +0200 Subject: [PATCH 32/43] feat(layout/inline-title): group some note types --- .../src/translations/en/translation.json | 3 +- .../client/src/widgets/layout/InlineTitle.tsx | 59 +++++++++++++++---- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 729d41a28..f2cceb725 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1754,7 +1754,8 @@ "placeholder": "type note's title here...", "created_on": "Created on ", "last_modified": "Last modified on ", - "note_type_switcher_label": "Switch from {{type}} to:" + "note_type_switcher_label": "Switch from {{type}} to:", + "note_type_switcher_others": "More note types" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 91335ed64..0d12e384f 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -8,12 +8,13 @@ import { Trans } from "react-i18next"; import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; -import { NOTE_TYPES } from "../../services/note_types"; +import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types"; import server from "../../services/server"; import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; -import { Badge } from "../react/Badge"; +import { Badge, BadgeWithDropdown } from "../react/Badge"; +import { FormListItem } from "../react/FormList"; import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; @@ -130,12 +131,26 @@ function TextWithValue({ i18nKey, value, valueTooltip }: { //#endregion //#region Note type switcher +const SWITCHER_PINNED_NOTE_TYPES = new Set([ "text", "code", "book", "canvas" ]); + function NoteTypeSwitcher() { const { note } = useNoteContext(); const blob = useNoteBlob(note); const currentNoteType = useNoteProperty(note, "type"); - const noteTypes = useMemo(() => NOTE_TYPES.filter((nt) => !nt.reserved && !nt.static), []); - const currentNoteTypeData = useMemo(() => noteTypes.find(t => t.type === currentNoteType), [ noteTypes, currentNoteType ]); + const { pinnedNoteTypes, restNoteTypes } = useMemo(() => { + const pinnedNoteTypes: NoteTypeMapping[] = []; + const restNoteTypes: NoteTypeMapping[] = []; + for (const noteType of NOTE_TYPES) { + if (noteType.reserved || noteType.static) continue; + if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) { + pinnedNoteTypes.push(noteType); + } else { + restNoteTypes.push(noteType); + } + } + return { pinnedNoteTypes, restNoteTypes }; + }, []); + const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); return (note?.type === "text" &&
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    - {blob?.contentLength === 0 && noteTypes.map(noteType => noteType.type !== currentNoteType && ( - server.put(`notes/${note.noteId}/type`, { type: noteType.type, mime: noteType.mime })} - /> - ))} + {blob?.contentLength === 0 && ( + <> + {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( + switchNoteType(note.noteId, noteType)} + /> + ))} + + {restNoteTypes.map(noteType => ( + switchNoteType(note.noteId, noteType)} + >{noteType.title} + ))} + + + )}
    ); } + +function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { + return server.put(`notes/${noteId}/type`, { type, mime }); +} //#endregion From e0f6ba808c1d9644446cc4bbb7bf7d80b6f40eb7 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 13:24:32 +0200 Subject: [PATCH 33/43] feat(layout/inline-title): template switcher --- .../src/translations/en/translation.json | 3 +- .../client/src/widgets/layout/InlineTitle.tsx | 63 +++++++++++++++---- 2 files changed, 53 insertions(+), 13 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index f2cceb725..822e7ef92 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1755,7 +1755,8 @@ "created_on": "Created on ", "last_modified": "Last modified on ", "note_type_switcher_label": "Switch from {{type}} to:", - "note_type_switcher_others": "More note types" + "note_type_switcher_others": "More note types", + "note_type_switcher_templates": "Templates" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 0d12e384f..5029a5ebd 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -6,6 +6,9 @@ import { ComponentChild } from "preact"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; +import FNote from "../../entities/fnote"; +import attributes from "../../services/attributes"; +import froca from "../../services/froca"; import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; import { NOTE_TYPES, NoteTypeMapping } from "../../services/note_types"; @@ -168,24 +171,60 @@ function NoteTypeSwitcher() { onClick={() => switchNoteType(note.noteId, noteType)} /> ))} - - {restNoteTypes.map(noteType => ( - switchNoteType(note.noteId, noteType)} - >{noteType.title} - ))} - + + )}
    ); } +function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteTypes: NoteTypeMapping[] }) { + return ( + + {restNoteTypes.map(noteType => ( + switchNoteType(noteId, noteType)} + >{noteType.title} + ))} + + ); +} + +function TemplateNoteTypes({ noteId }: { noteId: string }) { + const [ templates, setTemplates ] = useState([]); + + async function refreshTemplates() { + const templateNoteIds = await server.get("search-templates"); + const templateNotes = await froca.getNotes(templateNoteIds); + setTemplates(templateNotes); + } + + useEffect(() => { + refreshTemplates(); + }, []); + + return ( + + {templates.map(template => ( + attributes.setRelation(noteId, "template", template.noteId)} + >{template.title} + ))} + + ); +} + function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { return server.put(`notes/${noteId}/type`, { type, mime }); } From 700007696101e66b3592ee22ba04d2ea02f80e97 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 13:26:48 +0200 Subject: [PATCH 34/43] feat(layout/inline-title): react to template add/remove --- apps/client/src/widgets/layout/InlineTitle.tsx | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 5029a5ebd..8d13d1588 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -18,7 +18,7 @@ import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import { Badge, BadgeWithDropdown } from "../react/Badge"; import { FormListItem } from "../react/FormList"; -import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip } from "../react/hooks"; +import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; import { onWheelHorizontalScroll } from "../widget_utils"; @@ -205,9 +205,15 @@ function TemplateNoteTypes({ noteId }: { noteId: string }) { setTemplates(templateNotes); } - useEffect(() => { - refreshTemplates(); - }, []); + // First load. + useEffect(() => { refreshTemplates(); }, []); + + // React to external changes. + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (loadResults.getAttributeRows().some(attr => attr.type === "label" && attr.name === "template")) { + refreshTemplates(); + } + }); return ( Date: Sat, 13 Dec 2025 13:35:32 +0200 Subject: [PATCH 35/43] feat(layout/inline-title): support built-in templates --- .../client/src/widgets/layout/InlineTitle.tsx | 43 ++++++++++++++----- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 8d13d1588..de88b117c 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -17,7 +17,7 @@ import { formatDateTime } from "../../utils/formatters"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import { Badge, BadgeWithDropdown } from "../react/Badge"; -import { FormListItem } from "../react/FormList"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; import { useNoteBlob, useNoteContext, useNoteProperty, useStaticTooltip, useTriliumEvent } from "../react/hooks"; import { joinElements } from "../react/react_utils"; import { useNoteMetadata } from "../ribbon/NoteInfoTab"; @@ -197,16 +197,32 @@ function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteType } function TemplateNoteTypes({ noteId }: { noteId: string }) { - const [ templates, setTemplates ] = useState([]); + const [ userTemplates, setUserTemplates ] = useState([]); + const [ builtinTemplates, setBuiltinTemplates ] = useState([]); async function refreshTemplates() { const templateNoteIds = await server.get("search-templates"); const templateNotes = await froca.getNotes(templateNoteIds); - setTemplates(templateNotes); + setUserTemplates(templateNotes); + } + + async function loadBuiltinTemplates() { + const templatesRoot = await froca.getNote("_templates"); + if (!templatesRoot) return; + const childNotes = await templatesRoot.getChildNotes(); + const builtinTemplates: FNote[] = []; + for (const childNote of childNotes) { + if (childNote.hasLabel("collection") || !childNote.hasLabel("template")) continue; + builtinTemplates.push(childNote); + } + setBuiltinTemplates(builtinTemplates); } // First load. - useEffect(() => { refreshTemplates(); }, []); + useEffect(() => { + refreshTemplates(); + loadBuiltinTemplates(); + }, []); // React to external changes. useTriliumEvent("entitiesReloaded", ({ loadResults }) => { @@ -220,17 +236,22 @@ function TemplateNoteTypes({ noteId }: { noteId: string }) { text={t("note_title.note_type_switcher_templates")} icon="bx bx-copy-alt" > - {templates.map(template => ( - attributes.setRelation(noteId, "template", template.noteId)} - >{template.title} - ))} + {userTemplates.map(template => )} + {userTemplates.length > 0 && } + {builtinTemplates.map(template => )} ); } +function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) { + return ( + attributes.setRelation(noteId, "template", template.noteId)} + >{template.title} + ); +} + function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { return server.put(`notes/${noteId}/type`, { type, mime }); } From 0ffdedcfa6dae53eef776c0037946456bfcf3d3d Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 13:45:34 +0200 Subject: [PATCH 36/43] feat(layout/inline-title): dropdown for collections --- .../src/translations/en/translation.json | 3 +- .../client/src/widgets/layout/InlineTitle.tsx | 78 ++++++++++++++----- 2 files changed, 62 insertions(+), 19 deletions(-) diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 822e7ef92..986885a22 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1756,7 +1756,8 @@ "last_modified": "Last modified on ", "note_type_switcher_label": "Switch from {{type}} to:", "note_type_switcher_others": "More note types", - "note_type_switcher_templates": "Templates" + "note_type_switcher_templates": "Templates", + "note_type_switcher_collection": "Collections" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index de88b117c..ed5d9359a 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -144,7 +144,7 @@ function NoteTypeSwitcher() { const pinnedNoteTypes: NoteTypeMapping[] = []; const restNoteTypes: NoteTypeMapping[] = []; for (const noteType of NOTE_TYPES) { - if (noteType.reserved || noteType.static) continue; + if (noteType.reserved || noteType.static || noteType.type === "book") continue; if (SWITCHER_PINNED_NOTE_TYPES.has(noteType.type)) { pinnedNoteTypes.push(noteType); } else { @@ -154,6 +154,7 @@ function NoteTypeSwitcher() { return { pinnedNoteTypes, restNoteTypes }; }, []); const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); + const { builtinTemplates, collectionTemplates } = useBuiltinTemplates(); return (note?.type === "text" &&
    switchNoteType(note.noteId, noteType)} /> ))} + + - )}
    @@ -196,9 +198,25 @@ function MoreNoteTypes({ noteId, restNoteTypes }: { noteId: string, restNoteType ); } -function TemplateNoteTypes({ noteId }: { noteId: string }) { +function CollectionNoteTypes({ noteId, collectionTemplates }: { noteId: string, collectionTemplates: FNote[] }) { + return ( + + {collectionTemplates.map(collectionTemplate => ( + setTemplate(noteId, collectionTemplate.noteId)} + >{collectionTemplate.title} + ))} + + ); +} + +function TemplateNoteTypes({ noteId, builtinTemplates }: { noteId: string, builtinTemplates: FNote[] }) { const [ userTemplates, setUserTemplates ] = useState([]); - const [ builtinTemplates, setBuiltinTemplates ] = useState([]); async function refreshTemplates() { const templateNoteIds = await server.get("search-templates"); @@ -206,22 +224,9 @@ function TemplateNoteTypes({ noteId }: { noteId: string }) { setUserTemplates(templateNotes); } - async function loadBuiltinTemplates() { - const templatesRoot = await froca.getNote("_templates"); - if (!templatesRoot) return; - const childNotes = await templatesRoot.getChildNotes(); - const builtinTemplates: FNote[] = []; - for (const childNote of childNotes) { - if (childNote.hasLabel("collection") || !childNote.hasLabel("template")) continue; - builtinTemplates.push(childNote); - } - setBuiltinTemplates(builtinTemplates); - } - // First load. useEffect(() => { refreshTemplates(); - loadBuiltinTemplates(); }, []); // React to external changes. @@ -247,7 +252,7 @@ function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) return ( attributes.setRelation(noteId, "template", template.noteId)} + onClick={() => setTemplate(noteId, template.noteId)} >{template.title} ); } @@ -255,4 +260,41 @@ function TemplateItem({ noteId, template }: { noteId: string, template: FNote }) function switchNoteType(noteId: string, { type, mime }: NoteTypeMapping) { return server.put(`notes/${noteId}/type`, { type, mime }); } + +function setTemplate(noteId: string, templateId: string) { + return attributes.setRelation(noteId, "template", templateId); +} + +function useBuiltinTemplates() { + const [ templates, setTemplates ] = useState<{ + builtinTemplates: FNote[]; + collectionTemplates: FNote[]; + }>({ + builtinTemplates: [], + collectionTemplates: [] + }); + + async function loadBuiltinTemplates() { + const templatesRoot = await froca.getNote("_templates"); + if (!templatesRoot) return; + const childNotes = await templatesRoot.getChildNotes(); + const builtinTemplates: FNote[] = []; + const collectionTemplates: FNote[] = []; + for (const childNote of childNotes) { + if (!childNote.hasLabel("template")) continue; + if (childNote.hasLabel("collection")) { + collectionTemplates.push(childNote); + } else { + builtinTemplates.push(childNote); + } + } + setTemplates({ builtinTemplates, collectionTemplates }); + } + + useEffect(() => { + loadBuiltinTemplates(); + }, []); + + return templates; +} //#endregion From 56570d7ba157ebfbbf5bd2e7340b4a4548658aa3 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 13:46:28 +0200 Subject: [PATCH 37/43] fix(layout/inline-title): text displayed even when note is not empty --- apps/client/src/widgets/layout/InlineTitle.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index ed5d9359a..5738ffe8d 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -161,9 +161,9 @@ function NoteTypeSwitcher() { className="note-type-switcher" onWheel={onWheelHorizontalScroll} > -
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    {blob?.contentLength === 0 && ( <> +
    {t("note_title.note_type_switcher_label", { type: currentNoteTypeData?.title.toLocaleLowerCase() })}
    {pinnedNoteTypes.map(noteType => noteType.type !== currentNoteType && ( Date: Sat, 13 Dec 2025 13:57:54 +0200 Subject: [PATCH 38/43] feat(layout/inline-title): add transition --- .../client/src/widgets/layout/InlineTitle.css | 23 +++++++++++++++++++ .../client/src/widgets/layout/InlineTitle.tsx | 13 ++++++----- apps/client/src/widgets/note_title.css | 7 ------ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index a0e3f6edd..a740d11d7 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -20,6 +20,29 @@ } } +.note-split > .title-row .note-icon-widget, +.note-split > .title-row .note-title-widget, +.inline-title > .inline-title-row { + transition: opacity 200ms ease-in; +} + +.note-split { + &.inline-title-visible { + &> .title-row { + .note-icon-widget, + .note-title-widget { + opacity: 0; + } + } + } + + &:not(.inline-title-visible) { + .inline-title .inline-title-row { + opacity: 0; + } + } +} + .note-split.type-code:not(.mime-text-x-sqlite) .inline-title { background-color: var(--main-background-color); } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 5738ffe8d..5eb5e60cc 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -40,20 +40,21 @@ export default function InlineTitle() { useEffect(() => { if (!shown) return; - const titleRow = parentComponent.$widget[0] - .closest(".note-split") - ?.querySelector("&> .title-row"); - if (!titleRow) return; + const noteSplit = parentComponent.$widget[0].closest(".note-split"); + const titleRow = noteSplit?.querySelector("&> .title-row"); + if (!noteSplit || !titleRow) return; const observer = new IntersectionObserver((entries) => { - titleRow.classList.toggle("collapse", entries[0].isIntersecting); + noteSplit.classList.toggle("inline-title-visible", entries[0].isIntersecting); + }, { + threshold: 0.85 }); if (containerRef.current) { observer.observe(containerRef.current); } return () => { - titleRow.classList.remove("collapse"); + noteSplit.classList.remove("inline-title-visible"); observer.disconnect(); }; }, [ shown, parentComponent ]); diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 4f8176a1d..d08a04f46 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -51,12 +51,5 @@ body.experimental-feature-new-layout { display: none; } } - - &.collapse { - .note-icon-widget, - .note-title-widget { - visibility: hidden; - } - } } } From 0bd89a659c2e73177ada9a318fb80697ab08e232 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 14:00:27 +0200 Subject: [PATCH 39/43] chore(layout/inline-title): disable pointer events while hidden --- apps/client/src/widgets/layout/InlineTitle.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index a740d11d7..447178c52 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -32,6 +32,7 @@ .note-icon-widget, .note-title-widget { opacity: 0; + pointer-events: none; } } } @@ -39,6 +40,7 @@ &:not(.inline-title-visible) { .inline-title .inline-title-row { opacity: 0; + pointer-events: none; } } } From e296416a548750105b290c980bfc8144476e0280 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 14:38:58 +0200 Subject: [PATCH 40/43] fix(layout/inline-title): title not shown when switching to other types of notes --- .../client/src/widgets/layout/InlineTitle.css | 38 +++++++++---------- .../client/src/widgets/layout/InlineTitle.tsx | 22 ++++++----- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index 447178c52..a6000910e 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -1,3 +1,7 @@ +:root { + --title-transition: opacity 200ms ease-in; +} + .component.inline-title { contain: none; } @@ -9,6 +13,12 @@ & > .inline-title-row { display: flex; align-items: center; + transition: var(--title-transition); + + &.hidden { + opacity: 0; + pointer-events: none; + } } &.hidden { @@ -20,28 +30,16 @@ } } -.note-split > .title-row .note-icon-widget, -.note-split > .title-row .note-title-widget, -.inline-title > .inline-title-row { - transition: opacity 200ms ease-in; -} - -.note-split { - &.inline-title-visible { - &> .title-row { - .note-icon-widget, - .note-title-widget { - opacity: 0; - pointer-events: none; - } - } +.title-row { + &.note-icon-widget, + &.note-title-widget { + transition: var(--title-transition); } - &:not(.inline-title-visible) { - .inline-title .inline-title-row { - opacity: 0; - pointer-events: none; - } + &.hide-title .note-icon-widget, + &.hide-title .note-title-widget { + opacity: 0; + pointer-events: none; } } diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 5eb5e60cc..31f445504 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -3,7 +3,7 @@ import "./InlineTitle.css"; import { NoteType } from "@triliumnext/commons"; import clsx from "clsx"; import { ComponentChild } from "preact"; -import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; import { Trans } from "react-i18next"; import FNote from "../../entities/fnote"; @@ -31,21 +31,23 @@ export default function InlineTitle() { const { note, parentComponent, viewScope } = useNoteContext(); const type = useNoteProperty(note, "type"); const [ shown, setShown ] = useState(shouldShow(note?.noteId, type, viewScope)); - const containerRef= useRef(null); + const containerRef = useRef(null); + const [ titleHidden, setTitleHidden ] = useState(false); - useEffect(() => { + useLayoutEffect(() => { setShown(shouldShow(note?.noteId, type, viewScope)); }, [ note, type, viewScope ]); - useEffect(() => { + useLayoutEffect(() => { if (!shown) return; - const noteSplit = parentComponent.$widget[0].closest(".note-split"); - const titleRow = noteSplit?.querySelector("&> .title-row"); - if (!noteSplit || !titleRow) return; + const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector("&> .title-row"); + if (!titleRow) return; + titleRow.classList.toggle("hide-title", true); const observer = new IntersectionObserver((entries) => { - noteSplit.classList.toggle("inline-title-visible", entries[0].isIntersecting); + titleRow.classList.toggle("hide-title", entries[0].isIntersecting); + setTitleHidden(!entries[0].isIntersecting); }, { threshold: 0.85 }); @@ -54,7 +56,7 @@ export default function InlineTitle() { } return () => { - noteSplit.classList.remove("inline-title-visible"); + titleRow.classList.remove("hide-title"); observer.disconnect(); }; }, [ shown, parentComponent ]); @@ -64,7 +66,7 @@ export default function InlineTitle() { ref={containerRef} className={clsx("inline-title", !shown && "hidden")} > -
    +
    From 3851a944007628aa39c5478da7e30c3860c93e63 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 14:51:58 +0200 Subject: [PATCH 41/43] fix(layout/title_bar): badges not collapsing --- apps/client/src/widgets/note_title.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index d08a04f46..1b252a7f3 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -47,7 +47,7 @@ body.experimental-feature-new-layout { } .note-title-widget:focus-within + .note-badges, - .breadcrumb-badge .text { + .ext-badge .text { display: none; } } From 694cd2bc7c16d0ed7cabf70b7d1c71d8b476612c Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 14:58:11 +0200 Subject: [PATCH 42/43] chore(layout/title_bar): address LLM review --- apps/client/src/widgets/layout/InlineTitle.css | 2 +- apps/client/src/widgets/layout/InlineTitle.tsx | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index a6000910e..67bd4f980 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -22,7 +22,7 @@ } &.hidden { - display: none !important; + display: none; } .note-icon-widget { diff --git a/apps/client/src/widgets/layout/InlineTitle.tsx b/apps/client/src/widgets/layout/InlineTitle.tsx index 31f445504..7882ef202 100644 --- a/apps/client/src/widgets/layout/InlineTitle.tsx +++ b/apps/client/src/widgets/layout/InlineTitle.tsx @@ -41,7 +41,7 @@ export default function InlineTitle() { useLayoutEffect(() => { if (!shown) return; - const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector("&> .title-row"); + const titleRow = parentComponent.$widget[0].closest(".note-split")?.querySelector(":scope > .title-row"); if (!titleRow) return; titleRow.classList.toggle("hide-title", true); @@ -175,9 +175,9 @@ function NoteTypeSwitcher() { onClick={() => switchNoteType(note.noteId, noteType)} /> ))} - - - + {collectionTemplates.length > 0 && } + {builtinTemplates.length > 0 && } + {restNoteTypes.length > 0 && } )}
    From 6bf213a0b04fed19c5b69ad9b75fed42ea4ee949 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Sat, 13 Dec 2025 15:02:16 +0200 Subject: [PATCH 43/43] fix(layout/status_bar): some popups not dismissing --- apps/client/src/widgets/layout/StatusBar.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 07fd730d4..175aeeb53 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -12,6 +12,7 @@ import FNote from "../../entities/fnote"; import attributes from "../../services/attributes"; import { t } from "../../services/i18n"; import { ViewScope } from "../../services/link"; +import server from "../../services/server"; import { openInAppHelpFromUrl } from "../../services/utils"; import { formatDateTime } from "../../utils/formatters"; import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; @@ -28,7 +29,6 @@ import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; import { useAttachments } from "../type_widgets/Attachment"; import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector"; import Breadcrumb from "./Breadcrumb"; -import server from "../../services/server"; interface StatusBarContext { note: FNote; @@ -84,7 +84,6 @@ function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions ...titleOptions }} dropdownOptions={{ - autoClose: "outside", popperConfig: { strategy: "fixed", placement: "top" @@ -204,6 +203,7 @@ export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) { icon="bx bx-info-circle" title={t("status_bar.note_info_title")} dropdownContainerClassName="dropdown-note-info" + dropdownOptions={{ autoClose: "outside" }} >
      @@ -363,7 +363,6 @@ function CodeNoteSwitcher({ note }: StatusBarContext) { text={correspondingMimeType?.title} title={t("status_bar.code_note_switcher")} dropdownContainerClassName="dropdown-code-note-switcher" - dropdownOptions={{ autoClose: true }} >