Compare commits

..

4 Commits

Author SHA1 Message Date
Elian Doran
0e052bffe8 chore(react): add backup 2026-03-05 21:03:43 +02:00
Elian Doran
3fa2673b55 chore(react): integrate mobile layout as well 2026-03-05 20:59:59 +02:00
Elian Doran
a2130f4aa3 chore(react): port desktop version of note wrapper 2026-03-05 20:17:23 +02:00
Elian Doran
6c9246bc5b chore(react): get title bar to render 2026-03-05 20:10:26 +02:00
13 changed files with 137 additions and 163 deletions

View File

@@ -3,54 +3,29 @@ import type { WidgetsByParent } from "../services/bundle.js";
import { isExperimentalFeatureEnabled } from "../services/experimental_features.js";
import options from "../services/options.js";
import utils from "../services/utils.js";
import ApiLog from "../widgets/api_log.jsx";
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
import RightPaneToggle from "../widgets/buttons/right_pane_toggle.jsx";
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
import NoteList from "../widgets/collections/NoteList.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import FindWidget from "../widgets/find.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import StatusBar from "../widgets/layout/StatusBar.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.jsx";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import NoteWrapperWidget from "../widgets/NoteWrapper.js";
import QuickSearchWidget from "../widgets/quick_search.js";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import { FixedFormattingToolbar } from "../widgets/ribbon/FormattingToolbar.jsx";
import NoteActions from "../widgets/ribbon/NoteActions.jsx";
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
import TabRowWidget from "../widgets/tab_row.js";
import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx";
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
import TocWidget from "../widgets/toc.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import { applyModals } from "./layout_commons.js";
export default class DesktopLayout {
@@ -133,44 +108,7 @@ export default class DesktopLayout {
.filling()
.collapsible()
.id("center-pane")
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(new FlexContainer("row")
.class("title-row note-split-title")
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.optChild(isNewLayout, <NoteBadges />)
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.optChild(!isNewLayout, <MovePaneButton direction="left" />)
.optChild(!isNewLayout, <MovePaneButton direction="right" />)
.optChild(!isNewLayout, <ClosePaneButton />)
.optChild(!isNewLayout, <CreatePaneButton />)
.optChild(isNewLayout, <NoteActions />))
.optChild(!isNewLayout, <Ribbon />)
.child(new WatchedFileUpdateStatusWidget())
.optChild(!isNewLayout, <FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
.child(
new ScrollingContainer()
.filling()
.optChild(isNewLayout, <InlineTitle />)
.optChild(isNewLayout, <NoteTitleActions />)
.optChild(!isNewLayout, new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfo />)
)
.optChild(!isNewLayout, <PromotedAttributes />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<ScrollPadding />)
)
.child(<ApiLog />)
.child(new FindWidget())
.child(...this.customWidgets.get("note-detail-pane"))
)
)
.child(new SplitNoteContainer(() => <NoteWrapperWidget />))
.child(...this.customWidgets.get("center-pane"))
)

View File

@@ -3,29 +3,15 @@ import "./mobile_layout.css";
import type AppContext from "../components/app_context.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import FindWidget from "../widgets/find.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import NoteWrapperWidget from "../widgets/NoteWrapper";
import QuickSearchWidget from "../widgets/quick_search.js";
import ScrollPadding from "../widgets/scroll_padding";
import SearchResult from "../widgets/search_result.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import { applyModals } from "./layout_commons.js";
export default class MobileLayout {
@@ -52,35 +38,7 @@ export default class MobileLayout {
new ScreenContainer("detail", "row")
.id("detail-container")
.class("d-sm-flex d-md-flex d-lg-flex d-xl-flex col-12 col-sm-7 col-md-8 col-lg-9")
.child(
new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.class("title-row note-split-title")
.contentSized()
.css("align-items", "center")
.child(<ToggleSidebarButton />)
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(<NoteBadges />)
.child(<MobileDetailMenu />)
)
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(<InlineTitle />)
.child(<NoteTitleActions />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<SearchResult />)
.child(<ScrollPadding />)
)
.child(<MobileEditorToolbar />)
.child(new FindWidget())
)
)
.child(new SplitNoteContainer(() => <NoteWrapperWidget />))
)
)
.child(

View File

@@ -11,12 +11,6 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
private noteContext?: NoteContext;
constructor() {
super("column");
this.css("flex-grow", "1").collapsible();
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {
this.noteContext = noteContext;
@@ -53,8 +47,6 @@ export default class NoteWrapperWidget extends FlexContainer<BasicWidget> {
this.$widget.addClass("active");
}
this.$widget.addClass("component note-split");
const note = this.noteContext?.note;
if (!note) {
this.$widget.addClass("bgfx empty-note");

View File

@@ -0,0 +1,16 @@
.component.note-split {
display: flex;
flex-grow: 1;
flex-direction: column;
contain: none;
> .title-row {
display: flex;
flex-direction: row;
contain: none;
}
}
body.desktop .component.note-split > .title-row > * {
margin: 5px;
}

View File

@@ -0,0 +1,96 @@
import "./NoteWrapper.css";
import { isExperimentalFeatureEnabled } from "../services/experimental_features";
import { isDesktop } from "../services/utils";
import ApiLog from "./api_log";
import ClosePaneButton from "./buttons/close_pane_button";
import CreatePaneButton from "./buttons/create_pane_button";
import MovePaneButton from "./buttons/move_pane_button";
import NoteList from "./collections/NoteList";
import ScrollingContainer from "./containers/ScrollingContainer";
import FindWidget from "./find";
import FloatingButtons from "./FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS } from "./FloatingButtonsDefinitions";
import { LegacyWidgetRenderer } from "./launch_bar/LauncherDefinitions";
import SpacerWidget from "./launch_bar/SpacerWidget";
import InlineTitle from "./layout/InlineTitle";
import NoteBadges from "./layout/NoteBadges";
import NoteTitleActions from "./layout/NoteTitleActions";
import MobileDetailMenu from "./mobile_widgets/mobile_detail_menu";
import ToggleSidebarButton from "./mobile_widgets/toggle_sidebar_button";
import NoteIcon from "./note_icon";
import NoteTitleWidget from "./note_title";
import NoteDetail from "./NoteDetail";
import PromotedAttributes from "./PromotedAttributes";
import ReadOnlyNoteInfoBar from "./ReadOnlyNoteInfoBar";
import NoteActions from "./ribbon/NoteActions";
import Ribbon from "./ribbon/Ribbon";
import ScrollPadding from "./scroll_padding";
import SearchResult from "./search_result";
import SharedInfo from "./shared_info";
import MobileEditorToolbar from "./type_widgets/text/mobile_editor_toolbar";
import WatchedFileUpdateStatusWidget from "./watched_file_update_status";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
const cachedIsDesktop = isDesktop();
const cachedIsMobile = !cachedIsDesktop;
export default function NoteWrapper() {
return (
<div className="component note-split">
<TitleRow />
{!isNewLayout && <Ribbon />}
{cachedIsDesktop && <LegacyWidgetRenderer widget={new WatchedFileUpdateStatusWidget()} />}
{!isNewLayout && <FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />}
<ScrollingContainer>
{isNewLayout ? (
<>
<InlineTitle />
<NoteTitleActions />
</>
) : (
<>
<ReadOnlyNoteInfoBar />
<SharedInfo />
</>
)}
{!isNewLayout && <PromotedAttributes />}
<NoteDetail />
<NoteList media="screen" />
<SearchResult />
<ScrollPadding />
</ScrollingContainer>
<ApiLog />
{cachedIsMobile && <MobileEditorToolbar />}
<LegacyWidgetRenderer widget={new FindWidget()} />
</div>
);
}
function TitleRow() {
return (
<div className="component title-row note-split-title">
{cachedIsMobile && <ToggleSidebarButton />}
<NoteIcon />
<NoteTitleWidget />
{isNewLayout && <NoteBadges />}
{cachedIsDesktop ? (
<>
<SpacerWidget baseSize={0} growthFactor={1} />
{!isNewLayout ? (
<>
<MovePaneButton direction="left" />
<MovePaneButton direction="right" />
<ClosePaneButton />
<CreatePaneButton />
</>
) : (
<NoteActions />
)}
</>
) : (
<MobileDetailMenu />
)}
</div>
);
}

View File

@@ -10,8 +10,6 @@ export default class ScrollingContainer extends Container<BasicWidget> {
constructor() {
super();
this.class("scrolling-container");
}
setNoteContextEvent({ noteContext }: EventData<"setNoteContext">) {

View File

@@ -4,6 +4,7 @@
overflow: auto;
scroll-behavior: smooth;
position: relative;
flex-grow: 1;
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor,
> .note-list-widget:not(.full-height) .note-list-wrapper {

View File

@@ -0,0 +1,9 @@
import { ComponentChildren } from "preact";
export default function ScrollingContainer({ children }: { children: ComponentChildren }) {
return (
<div className="scrolling-container">
{children}
</div>
);
}

View File

@@ -1,9 +1,12 @@
import { VNode } from "preact";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import Component from "../../components/component.js";
import NoteContext from "../../components/note_context.js";
import splitService from "../../services/resizer.js";
import { isMobile } from "../../services/utils.js";
import type BasicWidget from "../basic_widget.js";
import { wrapReactWidgets } from "../basic_widget.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import FlexContainer from "./flex_container.js";
@@ -12,7 +15,7 @@ interface SplitNoteWidget extends BasicWidget {
ntxId?: string;
}
type WidgetFactory = () => SplitNoteWidget;
type WidgetFactory = () => (SplitNoteWidget | VNode);
export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
@@ -31,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer<SplitNoteWidget> {
}
async newNoteContextCreatedEvent({ noteContext }: EventData<"newNoteContextCreated">) {
const widget = this.widgetFactory();
const widget = wrapReactWidgets([ this.widgetFactory() ])[0];
const $renderedWidget = widget.render();

View File

@@ -1,6 +1,6 @@
import "./TableOfContents.css";
import { attributeChangeAffectsHeading, CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
import clsx from "clsx";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -170,14 +170,11 @@ function EditableTextTableOfContents() {
const affectsHeadings = changes.some( change => {
return (
change.type === 'insert' || change.type === 'remove' ||
(change.type === 'attribute' && attributeChangeAffectsHeading(change, textEditor))
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
);
});
if (affectsHeadings) {
requestAnimationFrame(() => {
setHeadings(extractTocFromTextEditor(textEditor));
});
setHeadings(extractTocFromTextEditor(textEditor));
}
};

View File

@@ -6,7 +6,7 @@ import "./MindMap.css";
import nodeMenu from "@mind-elixir/node-menu";
import { DISPLAYABLE_LOCALE_IDS } from "@triliumnext/commons";
import { snapdom } from "@zumer/snapdom";
import { DARK_THEME, default as VanillaMindElixir, MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME } from "mind-elixir";
import { default as VanillaMindElixir,MindElixirData, MindElixirInstance, Operation, Options, THEME as LIGHT_THEME, DARK_THEME } from "mind-elixir";
import { HTMLAttributes, RefObject } from "preact";
import { useCallback, useEffect, useRef } from "preact/hooks";
@@ -154,7 +154,6 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
const apiRef = useRef<MindElixirInstance>(null);
const [ locale ] = useTriliumOption("locale");
const colorScheme = useColorScheme();
const defaultColorScheme = useRef(colorScheme);
function reinitialize() {
if (!containerRef.current) return;
@@ -163,7 +162,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
el: containerRef.current,
locale: LOCALE_MAPPINGS[locale as DISPLAYABLE_LOCALE_IDS] ?? undefined,
editable,
theme: defaultColorScheme.current === "dark" ? DARK_THEME : LIGHT_THEME
theme: LIGHT_THEME
});
if (editable) {
@@ -189,11 +188,7 @@ function MindElixir({ containerRef: externalContainerRef, containerProps, apiRef
if (!apiRef.current) return;
const newTheme = colorScheme === "dark" ? DARK_THEME : LIGHT_THEME;
if (apiRef.current.theme === newTheme) return; // Avoid unnecessary theme changes, which can be expensive to render.
try {
apiRef.current.changeTheme(newTheme);
} catch (e) {
console.warn("Failed to change mind map theme:", e);
}
apiRef.current.changeTheme(newTheme);
}, [ colorScheme ]);
useEffect(() => {

View File

@@ -11,7 +11,6 @@ export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, Model
export type { TemplateDefinition } from "ckeditor5-premium-features";
export { default as buildExtraCommands } from "./extra_slash_commands.js";
export { default as getCkLocale } from "./i18n.js";
export * from "./utils.js";
// Import with sideffects to ensure that type augmentations are present.
import "@triliumnext/ckeditor5-math";

View File

@@ -1,28 +0,0 @@
import type { DifferItemAttribute, Editor, ModelDocumentFragment, ModelElement, ModelNode } from "ckeditor5";
function hasHeadingAncestor(node: ModelElement | ModelNode | ModelDocumentFragment | null): boolean {
let current: ModelElement | ModelNode | ModelDocumentFragment | null = node;
while (current) {
if (!!current && current.is('element') && (current as ModelElement).name.startsWith("heading")) return true;
current = current.parent;
}
return false;
}
export function attributeChangeAffectsHeading(change: DifferItemAttribute, editor: Editor): boolean {
if (change.type !== "attribute") return false;
// Fast checks on range boundaries
if (hasHeadingAncestor(change.range.start.parent) || hasHeadingAncestor(change.range.end.parent)) {
return true;
}
// Robust check across the whole changed range
const range = editor.model.createRange(change.range.start, change.range.end);
for (const item of range.getItems()) {
const baseNode = item.is("$textProxy") ? item.parent : item;
if (hasHeadingAncestor(baseNode)) return true;
}
return false;
}