diff --git a/.vscode/settings.json b/.vscode/settings.json index 57d22dcb8e..974a4ff64e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,5 +42,8 @@ }, "eslint.rules.customizations": [ { "rule": "*", "severity": "warn" } + ], + "cSpell.words": [ + "Trilium" ] -} \ No newline at end of file +} diff --git a/apps/build-docs/package.json b/apps/build-docs/package.json index ac5be7e9be..56fff334a8 100644 --- a/apps/build-docs/package.json +++ b/apps/build-docs/package.json @@ -11,7 +11,7 @@ "license": "AGPL-3.0-only", "packageManager": "pnpm@10.28.2", "devDependencies": { - "@redocly/cli": "2.15.0", + "@redocly/cli": "2.15.1", "archiver": "7.0.1", "fs-extra": "11.3.3", "react": "19.2.4", diff --git a/apps/client/index.html b/apps/client/index.html index e1db353327..12f653666f 100644 --- a/apps/client/index.html +++ b/apps/client/index.html @@ -13,6 +13,7 @@ +
diff --git a/apps/client/package.json b/apps/client/package.json index aedc4c26f0..dc210480a6 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -43,7 +43,7 @@ "debounce": "3.0.0", "draggabilly": "3.0.0", "force-graph": "1.51.0", - "globals": "17.2.0", + "globals": "17.3.0", "i18next": "25.8.0", "i18next-http-backend": "3.0.2", "jquery": "4.0.0", @@ -56,12 +56,12 @@ "mark.js": "8.11.1", "marked": "17.0.1", "mermaid": "11.12.2", - "mind-elixir": "5.6.1", + "mind-elixir": "5.7.1", "normalize.css": "8.0.1", "panzoom": "9.4.3", - "preact": "10.28.2", + "preact": "10.28.3", "react-i18next": "16.5.4", - "react-window": "2.2.5", + "react-window": "2.2.6", "reveal.js": "5.2.1", "svg-pan-zoom": "3.6.2", "tabulator-tables": "6.3.1", @@ -78,7 +78,7 @@ "@types/reveal.js": "5.2.2", "@types/tabulator-tables": "6.3.1", "copy-webpack-plugin": "13.0.1", - "happy-dom": "20.4.0", + "happy-dom": "20.5.0", "lightningcss": "1.31.1", "script-loader": "0.7.2", "vite-plugin-static-copy": "3.2.0" diff --git a/apps/client/src/layouts/mobile_layout.tsx b/apps/client/src/layouts/mobile_layout.tsx index da66ffa130..27b0779921 100644 --- a/apps/client/src/layouts/mobile_layout.tsx +++ b/apps/client/src/layouts/mobile_layout.tsx @@ -2,18 +2,20 @@ 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 ContentHeader from "../widgets/containers/content_header.js"; 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 FloatingButtons from "../widgets/FloatingButtons.jsx"; -import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx"; +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"; @@ -21,13 +23,10 @@ import NoteDetail from "../widgets/NoteDetail.jsx"; import PromotedAttributes from "../widgets/PromotedAttributes.jsx"; import QuickSearchWidget from "../widgets/quick_search.js"; import { useNoteContext } from "../widgets/react/hooks.jsx"; -import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx"; import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx"; import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx"; import SearchResult from "../widgets/search_result.jsx"; -import SharedInfoWidget from "../widgets/shared_info.js"; -import TabRowWidget from "../widgets/tab_row.js"; import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx"; import { applyModals } from "./layout_commons.js"; @@ -147,23 +146,22 @@ export default class MobileLayout { new NoteWrapperWidget() .child( new FlexContainer("row") + .class("title-row note-split-title") .contentSized() - .css("font-size", "larger") .css("align-items", "center") .child() + .child() .child() + .child() .child() ) - .child() .child() .child( new ScrollingContainer() .filling() .contentSized() - .child(new ContentHeader() - .child() - .child() - ) + .child() + .child() .child() .child() .child() @@ -171,6 +169,7 @@ export default class MobileLayout { .child() ) .child() + .child(new FindWidget()) ) ) ) @@ -179,7 +178,6 @@ export default class MobileLayout { new FlexContainer("column") .contentSized() .id("mobile-bottom-bar") - .child(new TabRowWidget().css("height", "40px")) .child(new FlexContainer("row") .class("horizontal") .css("height", "53px") diff --git a/apps/client/src/menus/context_menu.ts b/apps/client/src/menus/context_menu.ts index 6bd9de9e4c..415c0a2c6a 100644 --- a/apps/client/src/menus/context_menu.ts +++ b/apps/client/src/menus/context_menu.ts @@ -1,8 +1,9 @@ import { KeyboardActionNames } from "@triliumnext/commons"; +import { h, JSX, render } from "preact"; + import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js"; import note_tooltip from "../services/note_tooltip.js"; import utils from "../services/utils.js"; -import { h, JSX, render } from "preact"; export interface ContextMenuOptions { x: number; @@ -62,17 +63,17 @@ export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEve class ContextMenu { private $widget: JQuery; - private $cover: JQuery; + private $cover?: JQuery; private options?: ContextMenuOptions; private isMobile: boolean; constructor() { this.$widget = $("#context-menu-container"); - this.$cover = $("#context-menu-cover"); this.$widget.addClass("dropend"); this.isMobile = utils.isMobile(); if (this.isMobile) { + this.$cover = $("#context-menu-cover"); this.$cover.on("click", () => this.hide()); } else { $(document).on("click", (e) => this.hide()); @@ -91,7 +92,7 @@ class ContextMenu { } this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile); - this.$cover.addClass("show"); + this.$cover?.addClass("show"); $("body").addClass("context-menu-shown"); this.$widget.empty(); @@ -140,16 +141,14 @@ class ContextMenu { } else { left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET; } + } else if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) { + // Overflow: right + left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING; + } else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) { + // Overflow: left + left = CONTEXT_MENU_PADDING; } else { - if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) { - // Overflow: right - left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING; - } else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) { - // Overflow: left - left = CONTEXT_MENU_PADDING; - } else { - left = this.options.x - CONTEXT_MENU_OFFSET; - } + left = this.options.x - CONTEXT_MENU_OFFSET; } this.$widget @@ -249,7 +248,7 @@ class ContextMenu { if ("uiIcon" in item || "checked" in item) { const icon = (item.checked ? "bx bx-check" : item.uiIcon); if (icon) { - $icon.addClass(icon); + $icon.addClass([icon, "tn-icon"]); } else { $icon.append(" "); } @@ -261,7 +260,7 @@ class ContextMenu { .append(item.title); if ("badges" in item && item.badges) { - for (let badge of item.badges) { + for (const badge of item.badges) { const badgeElement = $(``).text(badge.title); if (badge.className) { @@ -352,7 +351,7 @@ class ContextMenu { async hide() { this.options?.onHide?.(); this.$widget.removeClass("show"); - this.$cover.removeClass("show"); + this.$cover?.removeClass("show"); $("body").removeClass("context-menu-shown"); this.$widget.hide(); } diff --git a/apps/client/src/services/css_class_manager.ts b/apps/client/src/services/css_class_manager.ts index de1c98b87d..06ff7b7045 100644 --- a/apps/client/src/services/css_class_manager.ts +++ b/apps/client/src/services/css_class_manager.ts @@ -49,7 +49,7 @@ function createClassForColor(colorString: string | null) { return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue"); } -function parseColor(color: string) { +export function parseColor(color: string) { try { return Color(color.toLowerCase()); } catch (ex) { @@ -77,7 +77,7 @@ function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: numb } /** Returns the hue of the specified color, or undefined if the color is grayscale. */ -function getHue(color: ColorInstance) { +export function getHue(color: ColorInstance) { const hslColor = color.hsl(); if (hslColor.saturationl() > 0) { return hslColor.hue(); diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts index 62d4ebb053..8cfbe126e8 100644 --- a/apps/client/src/services/experimental_features.ts +++ b/apps/client/src/services/experimental_features.ts @@ -1,5 +1,6 @@ import { t } from "./i18n"; import options from "./options"; +import { isMobile } from "./utils"; export interface ExperimentalFeature { id: string; @@ -21,7 +22,7 @@ let enabledFeatures: Set | null = null; export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean { if (featureId === "new-layout") { - return options.is("newLayout"); + return (isMobile() || options.is("newLayout")); } return getEnabledFeatures().has(featureId); @@ -29,7 +30,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): export function getEnabledExperimentalFeatureIds() { const values = [ ...getEnabledFeatures().values() ]; - if (options.is("newLayout")) { + if (isMobile() || options.is("newLayout")) { values.push("new-layout"); } return values; diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 17e431e542..e3d3f5279f 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -224,10 +224,6 @@ body.mobile .modal .modal-dialog { width: 100%; } -body.mobile .modal .modal-content { - border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0; -} - .component { contain: size; } @@ -458,7 +454,7 @@ body.desktop .tabulator-popup-container, visibility: hidden; } -body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item, +.dropdown-menu:not(#context-menu-container) .dropdown-item, body.desktop .dropdown-menu .dropdown-toggle, body #context-menu-container .dropdown-item > span, body.mobile .dropdown .dropdown-submenu > span { @@ -466,6 +462,15 @@ body.mobile .dropdown .dropdown-submenu > span { align-items: center; } + +body.mobile .dropdown .dropdown-submenu { + flex-wrap: wrap; + + & > span { + flex-grow: 1; + } +} + .dropdown-item span.keyboard-shortcut, .dropdown-item *:not(.keyboard-shortcut) > kbd { flex-grow: 1; @@ -1255,7 +1260,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href inset-inline-start: 0; inset-inline-end: 0; bottom: 0; - z-index: 1000; + z-index: 2500; background: rgba(0, 0, 0, 0.1); } @@ -1534,7 +1539,8 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { @media (max-width: 991px) { body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show, - body.mobile #launcher-container .dropdown > .dropdown-menu.show { + body.mobile #launcher-container .dropdown > .dropdown-menu.show, + body.mobile .dropdown-menu.mobile-bottom-menu.show { --dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)); position: fixed !important; bottom: var(--dropdown-bottom) !important; @@ -1546,6 +1552,10 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom)); } + body.mobile .modal-dialog .dropdown-menu.mobile-bottom-menu.show { + --dropdown-bottom: 0; + } + #mobile-sidebar-container { position: fixed; top: 0; @@ -1614,6 +1624,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { body.mobile .modal-content { overflow-y: auto; + border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0; } body.mobile .modal-footer { @@ -1669,6 +1680,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { #detail-container { background: var(--main-background-color); } + + .modal-dialog { + margin: var(--bs-modal-margin); + max-width: 80%; + } + + .modal-content { + height: 100%; + } } @media (max-width: 991px) { @@ -2617,14 +2637,14 @@ iframe.print-iframe { } } - #root-widget.virtual-keyboard-opened .note-split:not(:focus-within) { + /* #root-widget.virtual-keyboard-opened .note-split:not(:focus-within) { max-height: 80px; opacity: 0.4; - } + } */ } } -body.desktop .title-row { +.title-row { height: 50px; min-height: 50px; align-items: center; diff --git a/apps/client/src/stylesheets/theme-next-dark.css b/apps/client/src/stylesheets/theme-next-dark.css index 80acfe2e0a..f8fb305726 100644 --- a/apps/client/src/stylesheets/theme-next-dark.css +++ b/apps/client/src/stylesheets/theme-next-dark.css @@ -134,6 +134,7 @@ --left-pane-collapsed-border-color: #0009; --left-pane-background-color: #1f1f1f; --left-pane-text-color: #aaaaaa; + --left-pane-icon-color: #c5c5c5; --left-pane-item-hover-background: #ffffff0d; --left-pane-item-selected-background: #ffffff25; --left-pane-item-selected-color: #dfdfdf; diff --git a/apps/client/src/stylesheets/theme-next-light.css b/apps/client/src/stylesheets/theme-next-light.css index 1e50200d9a..2d7862ae00 100644 --- a/apps/client/src/stylesheets/theme-next-light.css +++ b/apps/client/src/stylesheets/theme-next-light.css @@ -127,6 +127,7 @@ --left-pane-collapsed-border-color: #0000000d; --left-pane-background-color: #f2f2f2; --left-pane-text-color: #383838; + --left-pane-icon-color: currentColor; --left-pane-item-hover-background: rgba(0, 0, 0, 0.032); --left-pane-item-selected-background: white; --left-pane-item-selected-color: black; diff --git a/apps/client/src/stylesheets/theme-next/notes/text.css b/apps/client/src/stylesheets/theme-next/notes/text.css index dc025bb66d..e025b23fb2 100644 --- a/apps/client/src/stylesheets/theme-next/notes/text.css +++ b/apps/client/src/stylesheets/theme-next/notes/text.css @@ -47,9 +47,14 @@ } /* The toolbar show / hide button for the current text block */ -.ck.ck-block-toolbar-button { +:root .ck.ck-block-toolbar-button { + --ck-color-block-toolbar-button: var(--muted-text-color); --ck-color-button-on-background: transparent; - --ck-color-button-on-color: currentColor; + --ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color); + translate: -40% 0; + min-width: 0; + padding: 0; + z-index: 1600; } :root .ck.ck-toolbar .ck-button:not(.ck-disabled):active, @@ -517,6 +522,10 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck * EDITOR'S CONTENT */ +.note-detail-editable-text-editor > .ck-placeholder { + opacity: .5; +} + /* * Code Blocks */ diff --git a/apps/client/src/stylesheets/theme-next/pages.css b/apps/client/src/stylesheets/theme-next/pages.css index f323d00005..e6125e80e2 100644 --- a/apps/client/src/stylesheets/theme-next/pages.css +++ b/apps/client/src/stylesheets/theme-next/pages.css @@ -156,6 +156,10 @@ --preferred-max-content-width: var(--options-card-max-width); } +.note-split.options .collection-properties { + visibility: hidden; +} + /* Create a gap at the top of the option pages */ .note-detail-content-widget-content.options>*:first-child { margin-top: var(--options-first-item-top-margin, 1em); diff --git a/apps/client/src/stylesheets/theme-next/shell.css b/apps/client/src/stylesheets/theme-next/shell.css index 7ff132e1af..fae4001f08 100644 --- a/apps/client/src/stylesheets/theme-next/shell.css +++ b/apps/client/src/stylesheets/theme-next/shell.css @@ -769,9 +769,9 @@ body.mobile .fancytree-node > span { #left-pane .fancytree-custom-icon { margin-top: 0; /* Use this to align the icon with the tree view item's caption */ + color: var(--custom-color, var(--left-pane-icon-color)); } - #left-pane span.fancytree-active .fancytree-title { font-weight: normal; } @@ -1272,7 +1272,7 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget { #center-pane .note-split { padding-top: 2px; background-color: var(--note-split-background-color, var(--main-background-color)); - transition: border-color 250ms ease-in; + transition: border-color 150ms ease-out; border: 2px solid transparent; } @@ -1322,7 +1322,7 @@ body.mobile .note-title { margin-inline-start: 0; } -.title-row { +body.desktop .title-row { /* Aligns the "Create new split" button with the note menu button (the three dots button) */ padding-inline-end: 3px; } diff --git a/apps/client/src/translations/ar/translation.json b/apps/client/src/translations/ar/translation.json index 296a0fdc86..91fbba92ce 100644 --- a/apps/client/src/translations/ar/translation.json +++ b/apps/client/src/translations/ar/translation.json @@ -29,7 +29,9 @@ "widget-render-error": { "title": "فشل عرض عنصر واجهة مستخدم React مخصص" }, - "widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك." + "widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.", + "open-script-note": "فتح ملاحظة برمجية", + "scripting-error": "خطأ في النص البرمجي المخصص: {{title}}" }, "add_link": { "add_link": "أضافة رابط", @@ -37,7 +39,9 @@ "search_note": "البحث عن الملاحظة بالاسم", "link_title": "عنوان الرابط", "button_add_link": "اضافة رابط", - "help_on_links": "مساعدة حول الارتباطات التشعبية" + "help_on_links": "مساعدة حول الارتباطات التشعبية", + "link_title_mirrors": "عنوان الرابط يعكس العنوان الحالي للملاحظة", + "link_title_arbitrary": "يمكن تغيير عنوان الرابط حسب الرغبة" }, "branch_prefix": { "edit_branch_prefix": "تعديل بادئة الفرع", diff --git a/apps/client/src/translations/cn/translation.json b/apps/client/src/translations/cn/translation.json index 9dd25e5984..83e8a25e75 100644 --- a/apps/client/src/translations/cn/translation.json +++ b/apps/client/src/translations/cn/translation.json @@ -1782,8 +1782,8 @@ "desktop-application": "桌面应用程序", "native-title-bar": "原生标题栏", "native-title-bar-description": "对于 Windows 和 macOS,关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。", - "background-effects": "启用背景效果(仅适用于 Windows 11)", - "background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。", + "background-effects": "启用背景效果", + "background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。", "restart-app-button": "重启应用程序以查看更改", "zoom-factor": "缩放系数" }, @@ -1802,7 +1802,8 @@ "geo-map": { "create-child-note-title": "创建一个新的子笔记并将其添加到地图中", "create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。", - "unable-to-load-map": "无法加载地图。" + "unable-to-load-map": "无法加载地图。", + "create-child-note-text": "添加标记" }, "geo-map-context": { "open-location": "打开位置", @@ -2117,7 +2118,7 @@ }, "call_to_action": { "background_effects_title": "背景效果现已推出稳定版本", - "background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。", + "background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。", "background_effects_button": "启用背景效果", "next_theme_title": "试用新 Trilium 主题", "next_theme_message": "当前使用旧版主题,要试用新主题吗?", @@ -2253,5 +2254,12 @@ "pages_alt": "第{{pageNumber}}页", "pages_loading": "加载中...", "layers_other": "{{count}} 层" + }, + "platform_indicator": { + "available_on": "在 {{platform}} 上可用" + }, + "mobile_tab_switcher": { + "title_other": "{{count}} 选项卡", + "more_options": "更多选项" } } diff --git a/apps/client/src/translations/de/translation.json b/apps/client/src/translations/de/translation.json index c250131841..42bd035e63 100644 --- a/apps/client/src/translations/de/translation.json +++ b/apps/client/src/translations/de/translation.json @@ -1771,7 +1771,8 @@ "geo-map": { "create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen", "create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.", - "unable-to-load-map": "Karte konnte nicht geladen werden." + "unable-to-load-map": "Karte konnte nicht geladen werden.", + "create-child-note-text": "Marker hinzufügen" }, "geo-map-context": { "open-location": "Ort öffnen", @@ -2270,5 +2271,10 @@ }, "platform_indicator": { "available_on": "Verfügbar auf {{platform}}" + }, + "mobile_tab_switcher": { + "title_one": "{{count}} Tab", + "title_other": "{{count}} Tabs", + "more_options": "Weitere Optionen" } } diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index d7d91b2443..3c2c5f6c0f 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -745,7 +745,7 @@ "button_title": "Export diagram as SVG" }, "relation_map_buttons": { - "create_child_note_title": "Create new child note and add it into this relation map", + "create_child_note_title": "Create child note and add it to map", "reset_pan_zoom_title": "Reset pan & zoom to initial coordinates and magnification", "zoom_in_title": "Zoom In", "zoom_out_title": "Zoom Out" @@ -760,7 +760,8 @@ "delete_this_note": "Delete this note", "note_revisions": "Note revisions", "error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'", - "error_unrecognized_command": "Unrecognized command {{command}}" + "error_unrecognized_command": "Unrecognized command {{command}}", + "backlinks": "Backlinks" }, "note_icon": { "change_note_icon": "Change note icon", @@ -2271,5 +2272,10 @@ }, "platform_indicator": { "available_on": "Available on {{platform}}" + }, + "mobile_tab_switcher": { + "title_one": "{{count}} tab", + "title_other": "{{count}} tabs", + "more_options": "More options" } } diff --git a/apps/client/src/translations/es/translation.json b/apps/client/src/translations/es/translation.json index dd3a083f71..aa0cbd188c 100644 --- a/apps/client/src/translations/es/translation.json +++ b/apps/client/src/translations/es/translation.json @@ -1614,7 +1614,7 @@ }, "bookmark_switch": { "bookmark": "Marcador", - "bookmark_this_note": "Añadir esta nota a marcadores en el panel lateral izquierdo", + "bookmark_this_note": "Agregar esta nota a marcadores en el panel lateral izquierdo", "remove_bookmark": "Eliminar marcador" }, "editability_select": { @@ -1964,7 +1964,8 @@ "geo-map": { "create-child-note-title": "Crear una nueva subnota y agregarla al mapa", "create-child-note-instruction": "Dé clic en el mapa para crear una nueva nota en esa ubicación o presione Escape para cancelar.", - "unable-to-load-map": "No se puede cargar el mapa." + "unable-to-load-map": "No se puede cargar el mapa.", + "create-child-note-text": "Agregar marcador" }, "geo-map-context": { "open-location": "Abrir ubicación", @@ -2284,5 +2285,11 @@ }, "platform_indicator": { "available_on": "Disponible en {{platform}}" + }, + "mobile_tab_switcher": { + "title_one": "{{count}} pestaña", + "title_many": "{{count}} pestañas", + "title_other": "{{count}} pestañas", + "more_options": "Más opciones" } } diff --git a/apps/client/src/translations/ga/translation.json b/apps/client/src/translations/ga/translation.json new file mode 100644 index 0000000000..ca517b61ea --- /dev/null +++ b/apps/client/src/translations/ga/translation.json @@ -0,0 +1,5 @@ +{ + "global_menu": { + "about": "Maidir le Trilium Notes" + } +} diff --git a/apps/client/src/translations/ja/translation.json b/apps/client/src/translations/ja/translation.json index 4d6f72caab..6c8414dbf7 100644 --- a/apps/client/src/translations/ja/translation.json +++ b/apps/client/src/translations/ja/translation.json @@ -2008,7 +2008,8 @@ "geo-map": { "create-child-note-title": "新しい子ノートを作成し、マップに追加する", "create-child-note-instruction": "地図をクリックしてその場所に新しいノートを作成するか、Esc キーを押して閉じます。", - "unable-to-load-map": "マップを読み込めません。" + "unable-to-load-map": "マップを読み込めません。", + "create-child-note-text": "マーカーを追加" }, "geo-map-context": { "open-location": "現在位置を表示", @@ -2256,5 +2257,9 @@ }, "platform_indicator": { "available_on": "{{platform}} で利用可能" + }, + "mobile_tab_switcher": { + "title_other": "{{count}} タブ", + "more_options": "その他のオプション" } } diff --git a/apps/client/src/translations/ro/translation.json b/apps/client/src/translations/ro/translation.json index 4823db67d3..843f90a03e 100644 --- a/apps/client/src/translations/ro/translation.json +++ b/apps/client/src/translations/ro/translation.json @@ -1761,8 +1761,8 @@ "show-recent-notes": "Afișează notițele recente" }, "electron_integration": { - "background-effects": "Activează efectele de fundal (doar pentru Windows 11)", - "background-effects-description": "Efectul Mica adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.", + "background-effects": "Activează efectele de fundal", + "background-effects-description": "Adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.", "desktop-application": "Aplicația desktop", "native-title-bar": "Bară de titlu nativă", "native-title-bar-description": "Pentru Windows și macOS, dezactivarea bării de titlu native face aplicația să pară mai compactă. Pe Linux, păstrarea bării integrează mai bine aplicația cu restul sistemului de operare.", @@ -1781,7 +1781,8 @@ "geo-map": { "create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă", "unable-to-load-map": "Nu s-a putut încărca harta.", - "create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula." + "create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula.", + "create-child-note-text": "Adaugă marcaj" }, "duration": { "days": "zile", @@ -2127,7 +2128,7 @@ }, "call_to_action": { "background_effects_title": "Efectele de fundal sunt acum stabile", - "background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.", + "background_effects_message": "Pe dispozitive cu Windows și macOS, efectele de fundal sunt stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei.", "background_effects_button": "Activează efectele de fundal", "next_theme_title": "Încercați noua temă Trilium", "next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?", @@ -2281,5 +2282,14 @@ "pages_other": "{{count}} de pagini", "pages_alt": "Pagina {{pageNumber}}", "pages_loading": "Încărcare..." + }, + "platform_indicator": { + "available_on": "Disponibil pe {{platform}}" + }, + "mobile_tab_switcher": { + "title_one": "{{count}} tab", + "title_few": "{{count}} taburi", + "title_other": "{{count}} de taburi", + "more_options": "Mai multe opțiuni" } } diff --git a/apps/client/src/widgets/Backlinks.css b/apps/client/src/widgets/Backlinks.css new file mode 100644 index 0000000000..9e8ced45ba --- /dev/null +++ b/apps/client/src/widgets/Backlinks.css @@ -0,0 +1,83 @@ +.tn-backlinks-widget .backlinks-items { + list-style-type: none; + margin: 0; + padding: 0; + position: static; + width: unset; + + > li { + --border-radius: 8px; + + max-width: 600px; + padding: 10px 20px; + background: var(--card-background-color); + + & + li { + margin-top: 2px; + } + + &:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + + &:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); + } + + /* Card header */ + & > span:first-child { + display: block; + + > span { + display: flex; + flex-wrap: wrap; + align-items: center; + + /* Note path */ + > small { + flex: 100%; + order: -1; + font-size: .65rem; + + .note-path { + padding: 0; + } + } + + /* Note icon */ + > .tn-icon { + color: var(--menu-item-icon-color); + } + + /* Note title */ + > a { + margin-inline-start: 4px; + color: currentColor; + font-weight: 500; + } + } + } + + /* Card content - excerpt */ + .backlink-excerpt { + all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */ + display: block; + + margin: 8px 0; + border-radius: 4px; + background: var(--quick-search-result-content-background); + padding: 8px; + font-size: .75rem; + + a { + background: transparent; + color: var(--quick-search-result-highlight-color); + text-decoration: underline; + } + + p { + margin: 0; + } + } + } +} diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index be8a41c46c..5d19389cf1 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -1,3 +1,5 @@ +import "./Backlinks.css"; + import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; import { VNode } from "preact"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; @@ -60,14 +62,6 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [ Backlinks ]; -export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [ - RefreshBackendLogButton, - EditButton, - RelationMapButtons, - ExportImageButtons, - Backlinks -]; - /** * Floating buttons that should be hidden in popup editor (Quick edit). */ diff --git a/apps/client/src/widgets/PromotedAttributes.tsx b/apps/client/src/widgets/PromotedAttributes.tsx index a9618d3a63..cbe5737256 100644 --- a/apps/client/src/widgets/PromotedAttributes.tsx +++ b/apps/client/src/widgets/PromotedAttributes.tsx @@ -5,6 +5,7 @@ import clsx from "clsx"; import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact"; import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks"; +import NoteContext from "../components/note_context"; import FAttribute from "../entities/fattribute"; import FNote from "../entities/fnote"; import { Attribute } from "../services/attribute_parser"; @@ -40,8 +41,8 @@ type OnChangeEventData = TargetedEvent | InputEvent | J type OnChangeListener = (e: OnChangeEventData) => Promise; export default function PromotedAttributes() { - const { note, componentId } = useNoteContext(); - const [ cells, setCells ] = usePromotedAttributeData(note, componentId); + const { note, componentId, noteContext } = useNoteContext(); + const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext); return ; } @@ -74,12 +75,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells } * * The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell. */ -export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch> ] { +export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch> ] { const [ viewType ] = useNoteLabel(note, "viewType"); const [ cells, setCells ] = useState(); function refresh() { - if (!note || viewType === "table") { + if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") { setCells([]); return; } @@ -124,7 +125,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone setCells(cells); } - useEffect(refresh, [ note, viewType ]); + useEffect(refresh, [ note, viewType, noteContext ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) { refresh(); diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index 255cf89c9f..0986c8ad29 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -29,7 +29,6 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: const isVerticalLayout = !isHorizontalLayout; const parentComponent = useContext(ParentComponent); const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus(); - const isMobileLocal = isMobile(); const logoRef = useRef(null); useStaticTooltip(logoRef); @@ -44,8 +43,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: } } noDropdownListStyle - onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined} - onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined} + mobileBackdrop > @@ -107,8 +105,7 @@ function BrowserOnlyOptions() { function DevelopmentOptions({ dropStart }: { dropStart: boolean }) { return <> - - Development Options + {experimentalFeatures.map((feature) => ( diff --git a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx index 61f79cc8da..61c7193a6e 100644 --- a/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx +++ b/apps/client/src/widgets/collections/legacy/ListOrGridView.tsx @@ -155,7 +155,7 @@ function NoteAttributes({ note }: { note: FNote }) { return ; } -function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: { +export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: { note: FNote; trim?: boolean; noChildrenList?: boolean; diff --git a/apps/client/src/widgets/containers/scrolling_container.css b/apps/client/src/widgets/containers/scrolling_container.css index 2a2adb1475..0e590801c4 100644 --- a/apps/client/src/widgets/containers/scrolling_container.css +++ b/apps/client/src/widgets/containers/scrolling_container.css @@ -1,14 +1,22 @@ .scrolling-container { + --content-margin-inline: 24px; + overflow: auto; scroll-behavior: smooth; position: relative; - > .inline-title, - > .note-detail > .note-detail-editable-text, + > .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor, > .note-list-widget:not(.full-height) .note-list-wrapper { - padding-inline: 24px; + margin-inline: var(--content-margin-inline); } + > .inline-title { + padding-inline: var(--content-margin-inline); + } + + > .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor { + overflow: unset; + } } .note-split.type-code:not(.mime-text-x-sqlite) { diff --git a/apps/client/src/widgets/dialogs/PopupEditor.css b/apps/client/src/widgets/dialogs/PopupEditor.css index 197ed94065..3357b00f3e 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.css +++ b/apps/client/src/widgets/dialogs/PopupEditor.css @@ -91,8 +91,9 @@ body.mobile .modal.popup-editor-dialog .modal-dialog { height: 100%; } -.modal.popup-editor-dialog .note-detail-editable-text { - padding: 0 1em; +.modal.popup-editor-dialog .note-detail-editable-text-editor { + margin: 0 28px; + overflow: visible; /* Allow selection rectangle to go outside of the editor area */ } .modal.popup-editor-dialog .note-detail-file { diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index e8e73ae785..a7f3fde393 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -5,13 +5,15 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import appContext from "../../components/app_context"; import NoteContext from "../../components/note_context"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; import froca from "../../services/froca"; import { t } from "../../services/i18n"; import tree from "../../services/tree"; import utils from "../../services/utils"; import NoteList from "../collections/NoteList"; import FloatingButtons from "../FloatingButtons"; -import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions"; +import { DESKTOP_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions"; +import NoteBadges from "../layout/NoteBadges"; import NoteIcon from "../note_icon"; import NoteTitleWidget from "../note_title"; import NoteDetail from "../NoteDetail"; @@ -23,8 +25,6 @@ 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 NoteBadges from "../layout/NoteBadges"; -import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; const isNewLayout = isExperimentalFeatureEnabled("new-layout"); @@ -34,7 +34,7 @@ export default function PopupEditor() { const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor")); const isMobile = utils.isMobile(); const items = useMemo(() => { - const baseItems = isMobile ? MOBILE_FLOATING_BUTTONS : DESKTOP_FLOATING_BUTTONS; + const baseItems = isMobile ? [] : DESKTOP_FLOATING_BUTTONS; return baseItems.filter(item => !POPUP_HIDDEN_FLOATING_BUTTONS.includes(item)); }, [ isMobile ]); diff --git a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx index 3450a4c013..d202feaf35 100644 --- a/apps/client/src/widgets/launch_bar/LauncherContainer.tsx +++ b/apps/client/src/widgets/launch_bar/LauncherContainer.tsx @@ -3,6 +3,7 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks"; import FNote from "../../entities/fnote"; import froca from "../../services/froca"; import { isDesktop, isMobile } from "../../services/utils"; +import TabSwitcher from "../mobile_widgets/TabSwitcher"; import { useTriliumEvent } from "../react/hooks"; import { onWheelHorizontalScroll } from "../widget_utils"; import BookmarkButtons from "./BookmarkButtons"; @@ -97,6 +98,8 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) { return ; case "aiChatLauncher": return ; + case "mobileTabSwitcher": + return ; default: throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); } diff --git a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx index 0cb7c9906f..bb4a053c89 100644 --- a/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx +++ b/apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import { createContext } from "preact"; import { useContext } from "preact/hooks"; @@ -18,12 +19,12 @@ export interface LauncherNoteProps { launcherNote: FNote; } -export function LaunchBarActionButton(props: Omit) { +export function LaunchBarActionButton({ className, ...props }: Omit) { const { isHorizontalLayout } = useContext(LaunchBarContext); return ( { diff --git a/apps/client/src/widgets/layout/StatusBar.css b/apps/client/src/widgets/layout/StatusBar.css index c421c3d65e..6418630dba 100644 --- a/apps/client/src/widgets/layout/StatusBar.css +++ b/apps/client/src/widgets/layout/StatusBar.css @@ -91,68 +91,6 @@ .note-paths-widget { padding: 0.5em; } - - .note-path-intro { - color: var(--muted-text-color); - } - - .note-path-list { - margin: 12px 0; - padding: 0; - list-style: none; - - /* Note path card */ - li { - --border-radius: 6px; - - position: relative; - background: var(--card-background-color); - padding: 8px 20px 8px 25px; - - &:first-child { - border-radius: var(--border-radius) var(--border-radius) 0 0; - } - - &:last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); - } - - & + li { - margin-top: 2px; - } - - /* Current path arrow */ - &.path-current::before { - position: absolute; - display: flex; - justify-content: flex-end; - align-items: center; - content: "\ee8f"; - top: 0; - left: 0; - width: 20px; - bottom: 0; - font-family: "boxicons"; - font-size: .75em; - color: var(--menu-item-icon-color); - } - } - - /* Note path segment */ - a { - margin-inline: 2px; - padding-inline: 2px; - color: currentColor; - font-weight: normal; - text-decoration: none; - - /* The last segment of the note path */ - &.basename { - color: var(--muted-text-color); - } - } - - } } .backlinks-widget > .dropdown-menu { @@ -160,84 +98,6 @@ max-height: 60vh; overflow-y: scroll; - - /* Backlink card */ - li { - --border-radius: 8px; - - max-width: 600px; - padding: 10px 20px; - background: var(--card-background-color); - - & + li { - margin-top: 2px; - } - - &:first-child { - border-radius: var(--border-radius) var(--border-radius) 0 0; - } - - &:last-child { - border-radius: 0 0 var(--border-radius) var(--border-radius); - } - - /* Card header */ - & > span:first-child { - display: block; - - > span { - display: flex; - flex-wrap: wrap; - align-items: center; - - /* Note path */ - > small { - flex: 100%; - order: -1; - font-size: .65rem; - - .note-path { - padding: 0; - } - } - - /* Note icon */ - > .tn-icon { - color: var(--menu-item-icon-color); - } - - /* Note title */ - > a { - margin-inline-start: 4px; - color: currentColor; - font-weight: 500; - } - } - } - - /* Card content - excerpt */ - & > span:nth-child(2) > div { - all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */ - display: block; - - margin: 8px 0; - border-radius: 4px; - background: var(--quick-search-result-content-background); - padding: 8px; - font-size: .75rem; - - a { - background: transparent; - color: var(--quick-search-result-highlight-color); - text-decoration: underline; - } - - p { - margin: 0; - } - } - - } } } diff --git a/apps/client/src/widgets/layout/StatusBar.tsx b/apps/client/src/widgets/layout/StatusBar.tsx index 903aeca3b2..bfc9b02648 100644 --- a/apps/client/src/widgets/layout/StatusBar.tsx +++ b/apps/client/src/widgets/layout/StatusBar.tsx @@ -56,7 +56,7 @@ export default function StatusBar() { similarNotesShown: activePane === "similar-notes", setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes") }; - const isHiddenNote = note?.isInHiddenSubtree(); + const isHiddenNote = note?.isHiddenCompletely(); return (
@@ -300,7 +300,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) { const count = useBacklinkCount(note, viewScope?.viewMode === "default"); return (note && count > 0 && .tn-icon { + margin-inline-end: 0.4em; + } + + .title { + overflow: hidden; + text-overflow: ellipsis; + text-wrap: nowrap; + font-size: 0.9em; + flex-grow: 1; + } + + .icon-action { + flex-shrink: 0; + } + } + + .tab-preview { + flex-grow: 1; + height: 100%; + overflow: hidden; + font-size: 0.5em; + user-select: none; + pointer-events: none; + + &.type-text { + padding: 10px; + } + + &.type-book, + &.type-contentWidget, + &.type-search, + &.type-empty { + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25em; + color: var(--muted-text-color); + } + + .preview-placeholder { + font-size: 500%; + } + + p { margin-bottom: 0.2em;} + h2 { font-size: 1.20em; } + h3 { font-size: 1.15em; } + h4 { font-size: 1.10em; } + h5 { font-size: 1.05em} + h6 { font-size: 1em; } + } + + &.with-split { + .preview-placeholder { + font-size: 250%; + } + } + } + } + + .modal-footer { + .tn-link { + color: var(--main-text-color); + width: 40%; + text-align: center; + text-decoration: none; + } + } +} diff --git a/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx new file mode 100644 index 0000000000..6ee84f0465 --- /dev/null +++ b/apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx @@ -0,0 +1,240 @@ +import "./TabSwitcher.css"; + +import clsx from "clsx"; +import { createPortal, Fragment } from "preact/compat"; +import { useCallback, useEffect, useRef, useState } from "preact/hooks"; + +import appContext, { CommandNames } from "../../components/app_context"; +import NoteContext from "../../components/note_context"; +import FNote from "../../entities/fnote"; +import contextMenu from "../../menus/context_menu"; +import { getHue, parseColor } from "../../services/css_class_manager"; +import froca from "../../services/froca"; +import { t } from "../../services/i18n"; +import { NoteContent } from "../collections/legacy/ListOrGridView"; +import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets"; +import { ICON_MAPPINGS } from "../note_bars/CollectionProperties"; +import ActionButton from "../react/ActionButton"; +import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks"; +import Icon from "../react/Icon"; +import LinkButton from "../react/LinkButton"; +import Modal from "../react/Modal"; + +export default function TabSwitcher() { + const [ shown, setShown ] = useState(false); + const mainNoteContexts = useMainNoteContexts(); + + return ( + <> + setShown(true)} + data-tab-count={mainNoteContexts.length > 99 ? "∞" : mainNoteContexts.length} + /> + {createPortal(, document.body)} + + ); +} + +function TabBarModal({ mainNoteContexts, shown, setShown }: { + mainNoteContexts: NoteContext[]; + shown: boolean; + setShown: (newValue: boolean) => void; +}) { + const [ fullyShown, setFullyShown ] = useState(false); + const selectTab = useCallback((noteContextToActivate: NoteContext) => { + appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId); + setShown(false); + }, [ setShown ]); + + return ( + setFullyShown(true)} + customTitleBarButtons={[ + { + iconClassName: "bx bx-dots-vertical-rounded", + title: t("mobile_tab_switcher.more_options"), + onClick(e) { + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items: [ + { title: t("tab_row.new_tab"), command: "openNewTab", uiIcon: "bx bx-plus" }, + { title: t("tab_row.reopen_last_tab"), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0 }, + { kind: "separator" }, + { title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-trash destructive-action-icon" }, + ], + selectMenuItemHandler: ({ command }) => { + if (command) { + appContext.triggerCommand(command); + } + } + }); + }, + } + ]} + footer={<> + { + appContext.triggerCommand("openNewTab"); + setShown(false); + }} + /> + } + scrollable + onHidden={() => { + setShown(false); + setFullyShown(false); + }} + > + + + ); +} + +function TabBarModelContent({ mainNoteContexts, selectTab, shown }: { + mainNoteContexts: NoteContext[]; + shown: boolean; + selectTab: (noteContextToActivate: NoteContext) => void; +}) { + const activeNoteContext = useActiveNoteContext(); + const tabRefs = useRef>({}); + + // Scroll to active tab. + useEffect(() => { + if (!shown || !activeNoteContext?.ntxId) return; + const correspondingEl = tabRefs.current[activeNoteContext.ntxId]; + requestAnimationFrame(() => { + correspondingEl?.scrollIntoView(); + }); + }, [ activeNoteContext, shown ]); + + return ( +
+ {mainNoteContexts.map((noteContext) => ( + (tabRefs.current[noteContext.ntxId ?? ""] = el)} + /> + ))} +
+ ); +} + +function Tab({ noteContext, containerRef, selectTab, activeNtxId }: { + containerRef: (el: HTMLDivElement | null) => void; + noteContext: NoteContext; + selectTab: (noteContextToActivate: NoteContext) => void; + activeNtxId: string | null | undefined; +}) { + const { note } = noteContext; + const iconClass = useNoteIcon(note); + const colorClass = note?.getColorClass() || ''; + const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext); + const subContexts = noteContext.getSubContexts(); + + return ( +
1 + })} + onClick={() => selectTab(noteContext)} + style={{ + "--bg-hue": workspaceTabBackgroundColorHue + }} + > + {subContexts.map(subContext => ( + +
+ {subContext.note && } + {subContext.note?.title ?? t("tab_row.new_tab")} + {subContext.isMainContext() && { + // We are closing a tab, so we need to prevent propagation for click (activate tab). + e.stopPropagation(); + appContext.tabManager.removeNoteContext(subContext.ntxId); + }} + />} +
+
+ +
+
+ ))} +
+ ); +} + +function TabPreviewContent({ note }: { + note: FNote | null +}) { + if (!note) { + return ; + } + + if (note.type === "book") { + return ; + } + + return ( + + ); +} + +function PreviewPlaceholder({ icon}: { + icon: string; +}) { + return ( +
+ +
+ ); +} + +function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) { + if (!noteContext.hoistedNoteId) return; + const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId); + if (!hoistedNote) return; + + const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor(); + if (!workspaceTabBackgroundColor) return; + + try { + const parsedColor = parseColor(workspaceTabBackgroundColor); + if (!parsedColor) return; + return getHue(parsedColor); + } catch (e) { + // Colors are non-critical, simply ignore. + console.warn(e); + } +} + +function useMainNoteContexts() { + const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts()); + + useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ] , () => { + setNoteContexts(appContext.tabManager.getMainNoteContexts()); + }); + + return noteContexts; +} diff --git a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx index 1566d08890..dc0c5e89cd 100644 --- a/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx +++ b/apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx @@ -1,84 +1,128 @@ -import { useContext } from "preact/hooks"; +import { createPortal, useState } from "preact/compat"; -import appContext, { CommandMappings } from "../../components/app_context"; -import contextMenu, { MenuItem } from "../../menus/context_menu"; -import branches from "../../services/branches"; +import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; -import { getHelpUrlForNote } from "../../services/in_app_help"; import note_create from "../../services/note_create"; -import tree from "../../services/tree"; -import { openInAppHelpFromUrl } from "../../services/utils"; -import BasicWidget from "../basic_widget"; +import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions"; import ActionButton from "../react/ActionButton"; -import { ParentComponent } from "../react/react_utils"; +import { FormDropdownDivider, FormListItem } from "../react/FormList"; +import { useNoteContext } from "../react/hooks"; +import Modal from "../react/Modal"; +import { NoteContextMenu } from "../ribbon/NoteActions"; +import NoteActionsCustom from "../ribbon/NoteActionsCustom"; +import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab"; export default function MobileDetailMenu() { - const parentComponent = useContext(ParentComponent); + const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext(); + const subContexts = noteContext?.getMainContext().getSubContexts() ?? []; + const isMainContext = noteContext?.isMainContext(); + const [ backlinksModalShown, setBacklinksModalShown ] = useState(false); + const [ notePathsModalShown, setNotePathsModalShown ] = useState(false); + const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); + const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default"); + + function closePane() { + // Wait first for the context menu to be dismissed, otherwise the backdrop stays on. + requestAnimationFrame(() => { + parentComponent.triggerCommand("closeThisNoteSplit", { ntxId }); + }); + } return ( - { - const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId(); - if (!ntxId) return; +
+ {note ? ( + +
+
+ setBacklinksModalShown(true)} + disabled={backlinksCount === 0} + >{t("status_bar.backlinks", { count: backlinksCount })} +
+
+ setNotePathsModalShown(true)} + disabled={(sortedNotePaths?.length ?? 0) <= 1} + >{t("status_bar.note_paths", { count: sortedNotePaths?.length })} +
+
+ - const noteContext = appContext.tabManager.getNoteContextById(ntxId); - const subContexts = noteContext.getMainContext().getSubContexts(); - const isMainContext = noteContext?.isMainContext(); - const note = noteContext.note; - const helpUrl = getHelpUrlForNote(note); + {noteContext && ntxId && } + noteContext?.notePath && note_create.createNote(noteContext.notePath)} + icon="bx bx-plus" + >{t("mobile_detail_menu.insert_child_note")} + {subContexts.length < 2 && <> + + parentComponent.triggerCommand("openNewNoteSplit", { ntxId })} + icon="bx bx-dock-right" + >{t("create_pane_button.create_new_split")} + } + {!isMainContext && <> + + {t("close_pane_button.close_this_pane")} + } + + } + /> + ) : ( + + )} - const items: (MenuItem)[] = [ - { title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" }, - { title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" }, - { kind: "separator" }, - { title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" }, - { kind: "separator" }, - helpUrl && { - title: t("help-button.title"), - uiIcon: "bx bx-help-circle", - handler: () => openInAppHelpFromUrl(helpUrl) - }, - { kind: "separator" }, - subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" }, - !isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" } - ].filter(i => !!i) as MenuItem[]; - - const lastItem = items.at(-1); - if (lastItem && "kind" in lastItem && lastItem.kind === "separator") { - items.pop(); - } - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items, - selectMenuItemHandler: async ({ command }) => { - if (command === "insertChildNote") { - note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined); - } else if (command === "delete") { - const notePath = appContext.tabManager.getActiveContextNotePath(); - if (!notePath) { - throw new Error("Cannot get note path to delete."); - } - - const branchId = await tree.getBranchIdFromUrl(notePath); - - if (!branchId) { - throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath })); - } - - if (await branches.deleteNotes([branchId]) && parentComponent) { - parentComponent.triggerCommand("setActiveScreen", { screen: "tree" }); - } - } else if (command && parentComponent) { - parentComponent.triggerCommand(command, { ntxId }); - } - }, - forcePositionOnMobile: true - }); - }} - /> + {createPortal(( + <> + + + + ), document.body)} +
+ ); +} + +function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) { + return ( + setModalShown(false)} + > +
    + {note && } +
+
+ ); +} + +function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined, setModalShown: (shown: boolean) => void }) { + return ( + setModalShown(false)} + > + {note && ( + + )} + ); } diff --git a/apps/client/src/widgets/note_bars/CollectionProperties.tsx b/apps/client/src/widgets/note_bars/CollectionProperties.tsx index d466e813cd..5dba675e6d 100644 --- a/apps/client/src/widgets/note_bars/CollectionProperties.tsx +++ b/apps/client/src/widgets/note_bars/CollectionProperties.tsx @@ -6,10 +6,7 @@ import { useContext, useRef } from "preact/hooks"; import { Fragment } from "preact/jsx-runtime"; import FNote from "../../entities/fnote"; -import { getHelpUrlForNote } from "../../services/in_app_help"; -import { openInAppHelpFromUrl } from "../../services/utils"; import { ViewTypeOptions } from "../collections/interface"; -import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList"; import FormTextBox from "../react/FormTextBox"; @@ -19,7 +16,7 @@ import { ParentComponent } from "../react/react_utils"; import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config"; import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab"; -const ICON_MAPPINGS: Record = { +export const ICON_MAPPINGS: Record = { grid: "bx bxs-grid", list: "bx bx-list-ul", calendar: "bx bx-calendar", diff --git a/apps/client/src/widgets/note_icon.css b/apps/client/src/widgets/note_icon.css index 4ed6df6482..cea05cdcd9 100644 --- a/apps/client/src/widgets/note_icon.css +++ b/apps/client/src/widgets/note_icon.css @@ -117,3 +117,35 @@ body.experimental-feature-new-layout { } } } + +body.mobile .modal.icon-switcher { + .modal-dialog { + left: 0; + right: 0; + margin: unset; + transform: unset; + max-width: 100%; + height: 100%; + } + + .modal-body { + padding: 0; + display: flex; + flex-direction: column; + + > .filter-row { + padding: 0.25em var(--bs-modal-padding) 0.5em var(--bs-modal-padding); + border-bottom: 1px solid var(--main-border-color); + } + } + + .icon-list { + margin: auto; + flex-grow: 1; + height: 100%; + + span { + padding: 12px; + } + } +} diff --git a/apps/client/src/widgets/note_icon.tsx b/apps/client/src/widgets/note_icon.tsx index 7bbbd3e345..9df9ad48f4 100644 --- a/apps/client/src/widgets/note_icon.tsx +++ b/apps/client/src/widgets/note_icon.tsx @@ -4,18 +4,24 @@ import { IconRegistry } from "@triliumnext/commons"; import { Dropdown as BootstrapDropdown } from "bootstrap"; import clsx from "clsx"; import { t } from "i18next"; -import { CSSProperties, RefObject } from "preact"; +import { CSSProperties } from "preact"; +import { createPortal } from "preact/compat"; import { useEffect, useMemo, useRef, useState } from "preact/hooks"; +import type React from "react"; import { CellComponentProps, Grid } from "react-window"; import FNote from "../entities/fnote"; import attributes from "../services/attributes"; import server from "../services/server"; +import { isDesktop, isMobile } from "../services/utils"; import ActionButton from "./react/ActionButton"; import Dropdown from "./react/Dropdown"; import { FormDropdownDivider, FormListItem } from "./react/FormList"; import FormTextBox from "./react/FormTextBox"; -import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks"; +import { useNoteContext, useNoteLabel, useStaticTooltip, useWindowSize } from "./react/hooks"; +import Modal from "./react/Modal"; + +const ICON_SIZE = isMobile() ? 56 : 48; interface IconToCountCache { iconClassToCountMap: Record; @@ -36,6 +42,10 @@ export default function NoteIcon() { setIcon(note?.getIcon()); }, [ note, iconClass, workspaceIconClass ]); + if (isMobile()) { + return ; + } + return ( - { note && } + { note && dropdownRef?.current?.hide()} columnCount={12} /> } ); } -function NoteIconList({ note, dropdownRef }: { - note: FNote, - dropdownRef: RefObject; +function MobileNoteIconSwitcher({ note, icon }: { + note: FNote | null | undefined; + icon: string | null | undefined; +}) { + const [ modalShown, setModalShown ] = useState(false); + const { windowWidth } = useWindowSize(); + + return (note && +
+ setModalShown(true)} + /> + + {createPortal(( + setModalShown(false)} + className="icon-switcher note-icon-widget" + scrollable + > + setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} /> + + ), document.body)} +
+ ); +} + +function NoteIconList({ note, onHide, columnCount }: { + note: FNote; + onHide: () => void; + columnCount: number; }) { - const searchBoxRef = useRef(null); const iconListRef = useRef(null); const [ search, setSearch ] = useState(); const [ filterByPrefix, setFilterByPrefix ] = useState(null); @@ -72,53 +113,22 @@ function NoteIconList({ note, dropdownRef }: { return ( <> -
- {t("note_icon.search")} - s.prefix === filterByPrefix)?.name ?? "" - }) - : t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })} - currentValue={search} onChange={setSearch} - autoFocus - /> - - {getIconLabels(note).length > 0 && ( -
- { - if (!note) return; - for (const label of getIconLabels(note)) { - attributes.removeAttributeById(note.noteId, label.attributeId); - } - dropdownRef?.current?.hide(); - }} - /> -
- )} - - {glob.iconRegistry.sources.length > 0 && - - } -
+
{ // Make sure we are not clicking on something else than a button. const clickedTarget = e.target as HTMLElement; @@ -129,18 +139,19 @@ function NoteIconList({ note, dropdownRef }: { const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass"; attributes.setLabel(note.noteId, attributeToSet, iconClass); } - dropdownRef?.current?.hide(); + onHide(); }} > {filteredIcons.length ? ( ) : ( @@ -151,12 +162,97 @@ function NoteIconList({ note, dropdownRef }: { ); } -function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{ +function FilterRow({ note, filterByPrefix, search, setSearch, setFilterByPrefix, filteredIcons, onHide }: { + note: FNote; + filterByPrefix: string | null; + search: string | undefined; + setSearch: (value: string | undefined) => void; + setFilterByPrefix: (value: string | null) => void; filteredIcons: IconWithName[]; -}>): React.JSX.Element { - const iconIndex = rowIndex * 12 + columnIndex; + onHide: () => void; +}) { + const searchBoxRef = useRef(null); + const hasCustomIcon = getIconLabels(note).length > 0; + + function resetToDefaultIcon() { + if (!note) return; + for (const label of getIconLabels(note)) { + attributes.removeAttributeById(note.noteId, label.attributeId); + } + onHide(); + } + + return ( +
+ {t("note_icon.search")} + s.prefix === filterByPrefix)?.name ?? "" + }) + : t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })} + currentValue={search} onChange={setSearch} + autoFocus + /> + + {isDesktop() + ? <> + {hasCustomIcon && ( +
+ +
+ )} + + { + + } + : ( + + {hasCustomIcon && <> + {t("note_icon.reset-default")} + + } + + + + )} +
+ ); +} + +function IconItemCell({ rowIndex, columnIndex, style, filteredIcons, columnCount }: CellComponentProps<{ + filteredIcons: IconWithName[]; + columnCount: number; +}>) { + const iconIndex = rowIndex * columnCount + columnIndex; const iconData = filteredIcons[iconIndex] as IconWithName | undefined; - if (!iconData) return <>; + if (!iconData) return <> as React.ReactElement; const { id, terms, iconPack } = iconData; return ( @@ -166,7 +262,7 @@ function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellCompo title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })} style={style as CSSProperties} /> - ); + ) as React.ReactElement; } function IconFilterContent({ filterByPrefix, setFilterByPrefix }: { @@ -183,7 +279,7 @@ function IconFilterContent({ filterByPrefix, setFilterByPrefix }: { checked={filterByPrefix === "bx"} onClick={() => setFilterByPrefix("bx")} >{t("note_icon.filter-default")} - + {glob.iconRegistry.sources.length > 1 && } {glob.iconRegistry.sources.map(({ prefix, name, icon }) => ( prefix !== "bx" && , "onClick" | "onAuxClick" | "onContextMenu"> { +import { CommandNames } from "../../components/app_context"; +import keyboard_actions from "../../services/keyboard_actions"; +import { useStaticTooltip } from "./hooks"; + +export interface ActionButtonProps extends Pick, "onClick" | "onAuxClick" | "onContextMenu" | "style"> { text: string; titlePosition?: "top" | "right" | "bottom" | "left"; icon: string; diff --git a/apps/client/src/widgets/react/Collapsible.css b/apps/client/src/widgets/react/Collapsible.css index 7dca2f5651..7acf628f71 100644 --- a/apps/client/src/widgets/react/Collapsible.css +++ b/apps/client/src/widgets/react/Collapsible.css @@ -17,21 +17,34 @@ .collapsible-body { height: 0; overflow: hidden; + + &.fully-expanded { + overflow: visible; + } } .collapsible-inner-body { padding-top: 0.5em; + opacity: 0; } &.expanded { .collapsible-title .arrow { transform: rotate(90deg); } + + .collapsible-inner-body { + opacity: 1; + } } &.with-transition { .collapsible-body { transition: height 250ms ease-in; } + + .collapsible-inner-body { + transition: opacity 250ms ease-in; + } } } diff --git a/apps/client/src/widgets/react/Collapsible.tsx b/apps/client/src/widgets/react/Collapsible.tsx index a6e1957b34..212de203dc 100644 --- a/apps/client/src/widgets/react/Collapsible.tsx +++ b/apps/client/src/widgets/react/Collapsible.tsx @@ -27,6 +27,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex const { height } = useElementSize(innerRef) ?? {}; const contentId = useUniqueName(); const [ transitionEnabled, setTransitionEnabled ] = useState(false); + const [ fullyExpanded, setFullyExpanded ] = useState(false); useEffect(() => { const timeout = setTimeout(() => { @@ -35,6 +36,21 @@ export function ExternallyControlledCollapsible({ title, children, className, ex return () => clearTimeout(timeout); }, []); + useEffect(() => { + if (expanded) { + if (transitionEnabled) { + const timeout = setTimeout(() => { + setFullyExpanded(true); + }, 250); + return () => clearTimeout(timeout); + } else { + setFullyExpanded(true); + } + } else { + setFullyExpanded(false); + } + }, [expanded, transitionEnabled]) + return (
diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 5af2f62280..407b14a63a 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -3,6 +3,7 @@ import { ComponentChildren, HTMLAttributes } from "preact"; import { CSSProperties, HTMLProps } from "preact/compat"; import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks"; +import { isMobile } from "../../services/utils"; import { useTooltip, useUniqueName } from "./hooks"; type DataAttributes = { @@ -32,9 +33,10 @@ export interface DropdownProps extends Pick, "id" | "c dropdownRef?: MutableRef; titlePosition?: "top" | "right" | "bottom" | "left"; titleOptions?: Partial; + mobileBackdrop?: boolean; } -export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) { +export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) { const containerRef = useRef(null); const triggerRef = useRef(null); const dropdownContainerRef = useRef(null); @@ -74,12 +76,18 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi setShown(true); externalOnShown?.(); hideTooltip(); - }, [ hideTooltip ]); + if (mobileBackdrop && isMobile()) { + document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover"); + } + }, [ hideTooltip, mobileBackdrop ]); const onHidden = useCallback(() => { setShown(false); externalOnHidden?.(); - }, []); + if (mobileBackdrop && isMobile()) { + document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover"); + } + }, [ mobileBackdrop ]); useEffect(() => { if (!containerRef.current) return; diff --git a/apps/client/src/widgets/react/FormFileUpload.tsx b/apps/client/src/widgets/react/FormFileUpload.tsx index e97e73184b..42d9c908eb 100644 --- a/apps/client/src/widgets/react/FormFileUpload.tsx +++ b/apps/client/src/widgets/react/FormFileUpload.tsx @@ -3,8 +3,9 @@ import { useEffect, useRef } from "preact/hooks"; import ActionButton, { ActionButtonProps } from "./ActionButton"; import Button, { ButtonProps } from "./Button"; +import { FormListItem, FormListItemOpts } from "./FormList"; -interface FormFileUploadProps { +export interface FormFileUploadProps { name?: string; onChange: (files: FileList | null) => void; multiple?: boolean; @@ -75,3 +76,25 @@ export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit ); } + +/** + * Similar to {@link FormFileUploadButton}, but uses an {@link FormListItem} instead of a normal {@link Button}. + * @param param the change listener for the file upload and the properties for the button. + */ +export function FormFileUploadFormListItem({ onChange, children, ...buttonProps }: Omit & Pick) { + const inputRef = useRef(null); + + return ( + <> + inputRef.current?.click()} + >{children} +
diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 6db1d384e5..51788085d2 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -1,6 +1,6 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons"; import { Dropdown as BootstrapDropdown } from "bootstrap"; -import { RefObject } from "preact"; +import { ComponentChildren, RefObject } from "preact"; import { useContext, useEffect, useRef } from "preact/hooks"; import appContext, { CommandNames } from "../../components/app_context"; @@ -63,7 +63,7 @@ function RevisionsButton({ note }: { note: FNote }) { type ItemToFocus = "basic-properties"; -function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { +export function NoteContextMenu({ note, noteContext, extraItems }: { note: FNote, noteContext?: NoteContext, extraItems?: ComponentChildren; }) { const dropdownRef = useRef(null); const parentComponent = useContext(ParentComponent); const noteType = useNoteProperty(note, "type") ?? ""; @@ -99,12 +99,15 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not dropdownRef={dropdownRef} buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" } className="note-actions" + dropdownContainerClassName="mobile-bottom-menu" hideToggleArrow noSelectButtonStyle noDropdownListStyle iconAction onHidden={() => itemToFocusRef.current = null } + mobileBackdrop > + {extraItems} {isReadOnly && <> void), disabled?: boolean, destructive?: boolean }) { +export function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) { return void; }) { return ( - parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })} @@ -161,7 +166,7 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode; return (isEnabled && - parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })} @@ -170,11 +175,11 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not } function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) { - const isShown = note.type === "mermaid" && note.isContentAvailable() && isDefaultViewMode; + const isShown = note.type === "mermaid" && !cachedIsMobile && note.isContentAvailable() && isDefaultViewMode; const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation"); const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal"; - return isShown && setSplitEditorOrientation(upcomingOrientation)} @@ -188,7 +193,7 @@ export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCus const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite) && note.isContentAvailable() && isDefaultViewMode; - return isEnabled && setReadOnly(!isReadOnly)} @@ -197,7 +202,7 @@ export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCus function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) { const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium"; - return isEnabled && openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")} @@ -239,7 +244,7 @@ function InAppHelpButton({ note }: NoteActionsCustomInnerProps) { const isEnabled = !!helpUrl; return isEnabled && ( - helpUrl && openInAppHelpFromUrl(helpUrl)} @@ -249,7 +254,7 @@ function InAppHelpButton({ note }: NoteActionsCustomInnerProps) { function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) { if (noteType === "relationMap") { - return parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })} @@ -258,3 +263,19 @@ function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteAc } } //#endregion + +function NoteAction({ text, ...props }: Pick & { + onClick?: ((e: MouseEvent) => void) | undefined; +}) { + return (cachedIsMobile + ? {text} + : + ); +} + +function NoteActionWithFileUpload({ text, ...props }: Pick & Pick) { + return (cachedIsMobile + ? {text} + : + ); +} diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.css b/apps/client/src/widgets/ribbon/NotePathsTab.css new file mode 100644 index 0000000000..64c7374480 --- /dev/null +++ b/apps/client/src/widgets/ribbon/NotePathsTab.css @@ -0,0 +1,63 @@ +body.experimental-feature-new-layout .note-paths-widget { + .note-path-intro { + color: var(--muted-text-color); + } + + .note-path-list { + margin: 12px 0; + padding: 0; + list-style: none; + + /* Note path card */ + li { + --border-radius: 6px; + + position: relative; + background: var(--card-background-color); + padding: 8px 20px 8px 25px; + + &:first-child { + border-radius: var(--border-radius) var(--border-radius) 0 0; + } + + &:last-child { + border-radius: 0 0 var(--border-radius) var(--border-radius); + } + + & + li { + margin-top: 2px; + } + + /* Current path arrow */ + &.path-current::before { + position: absolute; + display: flex; + justify-content: flex-end; + align-items: center; + content: "\ee8f"; + top: 0; + left: 0; + width: 20px; + bottom: 0; + font-family: "boxicons"; + font-size: .75em; + color: var(--menu-item-icon-color); + } + } + + /* Note path segment */ + a { + margin-inline: 2px; + padding-inline: 2px; + color: currentColor; + font-weight: normal; + text-decoration: none; + + /* The last segment of the note path */ + &.basename { + color: var(--muted-text-color); + } + } + + } +} diff --git a/apps/client/src/widgets/ribbon/NotePathsTab.tsx b/apps/client/src/widgets/ribbon/NotePathsTab.tsx index 19b361a5de..0b81ebe036 100644 --- a/apps/client/src/widgets/ribbon/NotePathsTab.tsx +++ b/apps/client/src/widgets/ribbon/NotePathsTab.tsx @@ -1,15 +1,16 @@ +import "./NotePathsTab.css"; + +import clsx from "clsx"; import { useEffect, useMemo, useState } from "preact/hooks"; import FNote, { NotePathRecord } from "../../entities/fnote"; import { t } from "../../services/i18n"; import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree"; import { useTriliumEvent } from "../react/hooks"; +import LinkButton from "../react/LinkButton"; import NoteLink from "../react/NoteLink"; import { joinElements } from "../react/react_utils"; import { TabContext } from "./ribbon-interface"; -import LinkButton from "../react/LinkButton"; -import clsx from "clsx"; - export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) { const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); @@ -112,9 +113,9 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
  • {joinElements(fullNotePaths.map((notePath, index, arr) => ( + className={clsx({"basename": (index === arr.length - 1)})} + notePath={notePath} + noPreview /> )), NOTE_PATH_TITLE_SEPARATOR)} {icons.map(({ icon, title }) => ( diff --git a/apps/client/src/widgets/type_widgets/options/other.tsx b/apps/client/src/widgets/type_widgets/options/other.tsx index 6ac92b4207..e6813f8d2b 100644 --- a/apps/client/src/widgets/type_widgets/options/other.tsx +++ b/apps/client/src/widgets/type_widgets/options/other.tsx @@ -1,20 +1,22 @@ +import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons"; +import { useMemo } from "preact/hooks"; +import type React from "react"; import { Trans } from "react-i18next"; + import { t } from "../../../services/i18n"; +import search from "../../../services/search"; import server from "../../../services/server"; import toast from "../../../services/toast"; +import { isElectron } from "../../../services/utils"; import Button from "../../react/Button"; -import FormText from "../../react/FormText"; -import OptionsSection from "./components/OptionsSection"; -import TimeSelector from "./components/TimeSelector"; -import { useMemo } from "preact/hooks"; -import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks"; -import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons"; import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; -import search from "../../../services/search"; -import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; import FormSelect from "../../react/FormSelect"; -import { isElectron } from "../../../services/utils"; +import FormText from "../../react/FormText"; +import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; +import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks"; +import OptionsSection from "./components/OptionsSection"; +import TimeSelector from "./components/TimeSelector"; export default function OtherSettings() { return ( @@ -31,7 +33,7 @@ export default function OtherSettings() { - ) + ); } function SearchEngineSettings() { @@ -82,7 +84,7 @@ function SearchEngineSettings() { /> - ) + ); } function TrayOptionsSettings() { @@ -97,7 +99,7 @@ function TrayOptionsSettings() { onChange={trayEnabled => setDisableTray(!trayEnabled)} /> - ) + ); } function NoteErasureTimeout() { @@ -105,13 +107,13 @@ function NoteErasureTimeout() { {t("note_erasure_timeout.note_erasure_description")} - {t("note_erasure_timeout.manual_erasing_description")} - +