diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index 076a76522..e3415bf4b 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -140,7 +140,7 @@ export default class DesktopLayout { .class("breadcrumb-row") .cssBlock(".breadcrumb-row > * { margin: 5px; }") .child() - .child() + .optChild(isNewLayout, ) .child() .child() .child() diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx index 3620d495d..50550ea4b 100644 --- a/apps/client/src/layouts/layout_commons.tsx +++ b/apps/client/src/layouts/layout_commons.tsx @@ -52,5 +52,5 @@ export function applyModals(rootContainer: RootContainer) { .child() .child() .child() - .child() + .child(); } diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 984349092..871b84bdc 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -1321,6 +1321,11 @@ body.desktop li.dropdown-submenu:hover > ul.dropdown-menu { overflow: auto; } +.dropdown-submenu.dropstart > .dropdown-menu { + inset-inline-start: auto; + inset-inline-end: calc(100% - 2px); +} + body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu { inset-inline-start: calc(-100% + 10px); } diff --git a/apps/client/src/stylesheets/theme-next/base.css b/apps/client/src/stylesheets/theme-next/base.css index 2f207ed44..78d432f19 100644 --- a/apps/client/src/stylesheets/theme-next/base.css +++ b/apps/client/src/stylesheets/theme-next/base.css @@ -89,13 +89,13 @@ * the color is adjusted based on the current color scheme (light or dark). The lightness * component of the color represented in the CIELAB color space, will be * constrained to a certain percentage defined below. - * + * * Note: the tree background may vary when background effects are enabled, so it is recommended * to maintain a higher contrast margin than on the usual note tree solid background. */ /* The maximum perceptual lightness for the custom color in the light theme (%): */ --tree-item-light-theme-max-color-lightness: 60; - + /* The minimum perceptual lightness for the custom color in the dark theme (%): */ --tree-item-dark-theme-min-color-lightness: 65; } @@ -165,7 +165,7 @@ body.desktop .dropdown-submenu .dropdown-menu { --menu-item-start-padding: 8px; --menu-item-end-padding: 22px; --menu-item-vertical-padding: 2px; - + padding-top: var(--menu-item-vertical-padding) !important; padding-bottom: var(--menu-item-vertical-padding) !important; padding-inline-start: var(--menu-item-start-padding) !important; @@ -176,6 +176,11 @@ body.desktop .dropdown-submenu .dropdown-menu { cursor: default !important; } +.dropdown-menu:has(> .dropdown-submenu.dropstart) > .dropdown-item { + padding-inline-end: var(--menu-item-start-padding) !important; + padding-inline-start: var(--menu-item-end-padding) !important; +} + :root .dropdown-item:focus-visible { outline: 2px solid var(--input-focus-outline-color) !important; background-color: transparent; @@ -249,7 +254,7 @@ html body .dropdown-item[disabled] { } /* Menu item arrow */ -.dropdown-menu .dropdown-toggle::after { +.dropdown-submenu:not(.dropstart) .dropdown-toggle::after { content: "\ed3b" !important; position: absolute; display: flex !important; @@ -265,6 +270,22 @@ html body .dropdown-item[disabled] { color: var(--menu-item-arrow-color) !important; } +.dropdown-submenu.dropstart .dropdown-toggle::before { + content: "\ea4d" !important; + position: absolute; + display: flex !important; + align-items: center; + justify-content: center; + top: 0; + inset-inline-start: 0; + margin: unset !important; + border: unset !important; + padding: 0 4px; + font-family: boxicons; + font-size: 1.2em; + color: var(--menu-item-arrow-color) !important; +} + body[dir=rtl] .dropdown-menu:not([data-popper-placement="bottom-start"]) .dropdown-toggle::after { content: "\ea4d" !important; } @@ -339,7 +360,7 @@ body.mobile .dropdown-menu { font-size: 1em !important; backdrop-filter: var(--dropdown-backdrop-filter); position: relative; - + .dropdown-toggle::after { top: 0.5em; right: var(--dropdown-menu-padding-horizontal); @@ -356,7 +377,7 @@ body.mobile .dropdown-menu { padding: var(--dropdown-menu-padding-vertical) var(--dropdown-menu-padding-horizontal) !important; background: var(--card-background-color); border-bottom: 1px solid var(--menu-item-delimiter-color) !important; - border-radius: 0; + border-radius: 0; } .dropdown-item:first-of-type, @@ -367,9 +388,9 @@ body.mobile .dropdown-menu { border-top-right-radius: 6px; } - .dropdown-item:last-of-type, + .dropdown-item:last-of-type, .dropdown-item:has(+ .dropdown-divider), - .dropdown-custom-item:last-of-type, + .dropdown-custom-item:last-of-type, .dropdown-custom-item:has(+ .dropdown-divider) { border-bottom-left-radius: 6px; border-bottom-right-radius: 6px; @@ -392,10 +413,10 @@ body.mobile .dropdown-menu { --menu-background-color: --menu-submenu-mobile-background-color; --bs-dropdown-divider-margin-y: 0.25rem; border-radius: 0; - max-height: 0; + max-height: 0; transition: max-height 100ms ease-in; - display: block !important; - + display: block !important; + &.show { max-height: 1000px; padding: 0.5rem 0.75rem !important; @@ -405,7 +426,7 @@ body.mobile .dropdown-menu { &.submenu-open { .dropdown-toggle { padding-bottom: var(--dropdown-menu-padding-vertical); - } + } } } @@ -743,4 +764,4 @@ li.dropdown-item a.dropdown-item-button:focus-visible { .note-detail-empty .aa-suggestions div.aa-cursor { background: var(--hover-item-background-color); color: var(--hover-item-text-color); -} \ No newline at end of file +} diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 3dcd231bc..2298f2f2b 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -689,6 +689,7 @@ "export_note": "Export note", "delete_note": "Delete note", "print_note": "Print note", + "view_revisions": "Note revisions...", "save_revision": "Save revision", "convert_into_attachment_failed": "Converting note '{{title}}' failed.", "convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.", @@ -1750,8 +1751,8 @@ }, "note_title": { "placeholder": "type note's title here...", - "created_on": "Created on {{date}}", - "last_modified": "Last modified on {{date}}" + "created_on": "Created on ", + "last_modified": "Last modified on " }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", @@ -2132,8 +2133,18 @@ }, "breadcrumb_badges": { "read_only_explicit": "Read-only", + "read_only_explicit_description": "This note has been manually set to read-only.\nClick to edit it temporarily.", "read_only_auto": "Auto read-only", + "read_only_auto_description": "This note was set automatically to read-only mode for performance reasons. This automatic limit is adjustable from settings.\n\nClick to edit it temporarily.", + "read_only_temporarily_disabled": "Temporarily editable", + "read_only_temporarily_disabled_description": "This note is currently editable, but it is normally read-only. The note will go back to being read-only as soon as you navigate to another note.\n\nClick to re-enable read-only mode.", "shared_publicly": "Shared publicly", - "shared_locally": "Shared locally" + "shared_publicly_description": "This note has been published online at {{- link}}, and is publicly accessible.\n\nClick to navigate to the shared note or right click for more options.", + "shared_locally": "Shared locally", + "shared_locally_description": "This note is shared on the local network only at {{- link}}.\n\nClick to navigate to the shared note or right click for more options.", + "backlinks_one": "{{count}} backlink", + "backlinks_other": "{{count}} backlinks", + "backlinks_description_one": "This note is linked from {{count}} other note.\n\nClick to view the list of backlinks.", + "backlinks_description_other": "This note is linked from {{count}} other notes.\n\nClick to view the list of backlinks." } } diff --git a/apps/client/src/widgets/Breadcrumb.css b/apps/client/src/widgets/Breadcrumb.css index 5f3bc886b..2bb58281f 100644 --- a/apps/client/src/widgets/Breadcrumb.css +++ b/apps/client/src/widgets/Breadcrumb.css @@ -4,6 +4,49 @@ min-height: 30px; align-items: center; padding: 10px; + container-type: inline-size; + + @container (max-width: 700px) { + .breadcrumb-badges { + flex-shrink: 0; + + >* { + flex-shrink: 0; + width: 18px; + } + + .dropdown { + button { + flex-shrink: 0; + } + } + + .breadcrumb-badge { + flex-shrink: 0; + padding: 0 2px; + + >* { + text-overflow: clip; + } + + .text { + display: none; + } + } + } + } + + @container (max-width: 500px) { + .breadcrumb { + .btn.icon-action { + width: 16px; + } + } + + .icon-action { + margin: 0; + } + } } body.experimental-feature-new-layout .breadcrumb-row { @@ -53,11 +96,23 @@ body.experimental-feature-new-layout .breadcrumb-row { } .dropdown-item span, - .dropdown-item strong { + .dropdown-item strong, + .breadcrumb-last-item { text-overflow: ellipsis; white-space: nowrap; overflow: hidden; display: block; max-width: 300px; } + + .breadcrumb-last-item { + text-decoration: none; + color: unset; + cursor: text; + } + + input { + padding: 0 10px; + width: 200px; + } } diff --git a/apps/client/src/widgets/Breadcrumb.tsx b/apps/client/src/widgets/Breadcrumb.tsx index 38efb1364..83ff966b8 100644 --- a/apps/client/src/widgets/Breadcrumb.tsx +++ b/apps/client/src/widgets/Breadcrumb.tsx @@ -1,6 +1,6 @@ import "./Breadcrumb.css"; -import { useMemo } from "preact/hooks"; +import { useMemo, useState } from "preact/hooks"; import { Fragment } from "preact/jsx-runtime"; import NoteContext from "../components/note_context"; @@ -12,6 +12,8 @@ import { useChildNotes, useNoteContext, useNoteLabel, useNoteProperty } from "./ import Icon from "./react/Icon"; import NoteLink from "./react/NoteLink"; import link_context_menu from "../menus/link_context_menu"; +import { TitleEditor } from "./collections/board"; +import server from "../services/server"; const COLLAPSE_THRESHOLD = 5; const INITIAL_ITEMS = 2; @@ -27,10 +29,7 @@ export default function Breadcrumb() { <> {notePath.slice(0, INITIAL_ITEMS).map((item, index) => ( - {index === 0 - ? - : - } + ))} @@ -38,7 +37,7 @@ export default function Breadcrumb() { {notePath.slice(-FINAL_ITEMS).map((item, index) => ( - + ))} @@ -47,7 +46,7 @@ export default function Breadcrumb() { {index === 0 ? - : + : } {(index < notePath.length - 1 || note?.hasChildren()) && } @@ -76,15 +75,56 @@ function BreadcrumbRoot({ noteContext }: { noteContext: NoteContext | undefined ); } -function BreadcrumbItem({ notePath }: { notePath: string }) { +function BreadcrumbLink({ notePath }: { notePath: string }) { return ( ); } +function BreadcrumbLastItem({ notePath }: { notePath: string }) { + const noteId = notePath.split("/").at(-1); + const [ note ] = useState(() => froca.getNoteFromCache(noteId!)); + const [ isEditing, setIsEditing ] = useState(false); + const title = useNoteProperty(note, "title"); + + if (!note) return null; + + if (!isEditing) { + return ( + { + e.preventDefault(); + setIsEditing(true); + }} + >{title} + ); + } + + return ( + { return server.put(`notes/${noteId}/title`, { title: newTitle.trim() }); }} + dismiss={() => setIsEditing(false)} + /> + ); +} + +function BreadcrumbItem({ index, notePath, noteContext, notePathLength }: { index: number, notePathLength: number, notePath: string, noteContext: NoteContext | undefined }) { + if (index === 0) { + return ; + } + + if (index === notePathLength - 1) { + return ; + } + + return ; +} + function BreadcrumbSeparator({ notePath, noteContext, activeNotePath }: { notePath: string, activeNotePath: string, noteContext: NoteContext | undefined }) { return ( * { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .dropdown { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: var(--badge-radius); + + &.dropdown-backlinks-badge .dropdown-menu { + min-width: 500px; + } + + .breadcrumb-badge { + border-radius: 0; + } + + .btn { + border: 0; + margin: 0; + padding: 0; + } } } diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/BreadcrumbBadges.tsx index 3bb3c06b6..57377167e 100644 --- a/apps/client/src/widgets/BreadcrumbBadges.tsx +++ b/apps/client/src/widgets/BreadcrumbBadges.tsx @@ -1,17 +1,22 @@ import "./BreadcrumbBadges.css"; -import { ComponentChildren } from "preact"; -import { useIsNoteReadOnly, useNoteContext } from "./react/hooks"; +import clsx from "clsx"; +import { ComponentChildren, MouseEventHandler } from "preact"; +import { useRef } from "preact/hooks"; + +import { t } from "../services/i18n"; +import { BacklinksList, useBacklinkCount } from "./FloatingButtonsDefinitions"; +import Dropdown, { DropdownProps } from "./react/Dropdown"; +import { useIsNoteReadOnly, useNoteContext, useStaticTooltip } from "./react/hooks"; import Icon from "./react/Icon"; import { useShareInfo } from "./shared_info"; -import clsx from "clsx"; -import { t } from "../services/i18n"; export default function BreadcrumbBadges() { return (
+
); } @@ -20,37 +25,113 @@ function ReadOnlyBadge() { const { note, noteContext } = useNoteContext(); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); const isExplicitReadOnly = note?.isLabelTruthy("readOnly"); + const isTemporarilyEditable = noteContext?.viewScope?.readOnlyTemporarilyDisabled; - return (isReadOnly && - enableEditing()}> - {isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")} - - ); + if (isTemporarilyEditable) { + return enableEditing(false)} + />; + } else if (isReadOnly) { + return enableEditing()} + />; + } } function ShareBadge() { const { note } = useNoteContext(); - const { isSharedExternally, link } = useShareInfo(note); + const { isSharedExternally, link, linkHref } = useShareInfo(note); return (link && - {isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")} - + text={isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")} + tooltip={isSharedExternally ? + t("breadcrumb_badges.shared_publicly_description", { link }) : + t("breadcrumb_badges.shared_locally_description", { link }) + } + className="share-badge" + href={linkHref} + /> ); } -function Badge({ icon, children, onClick }: { icon: string, children: ComponentChildren, onClick?: () => void }) { +function BacklinksBadge() { + const { note, viewScope } = useNoteContext(); + const count = useBacklinkCount(note, viewScope?.viewMode === "default"); + return (note && count > 0 && + + + + ); +} + +interface BadgeProps { + text: string; + icon?: string; + className: string; + tooltip?: string; + onClick?: MouseEventHandler; + href?: string; +} + +function Badge({ icon, className, text, tooltip, onClick, href }: BadgeProps) { + const containerRef = useRef(null); + useStaticTooltip(containerRef, { + placement: "bottom", + fallbackPlacements: [ "bottom" ], + animation: false, + html: true, + title: tooltip + }); + + const content = <> + {icon && <> } + {text} + ; + return (
-   - {children} + {href ? {content} : {content}}
); } + +function BadgeWithDropdown({ children, tooltip, className, dropdownOptions, ...props }: BadgeProps & { + children: ComponentChildren, + dropdownOptions?: Partial +}) { + return ( + } + noDropdownListStyle + noSelectButtonStyle + hideToggleArrow + title={tooltip} + titlePosition="bottom" + dropdownOptions={{ popperConfig: { placement: "bottom", strategy: "fixed" } }} + {...dropdownOptions} + >{children} + ); +} diff --git a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx index 65743984e..1c7a904a9 100644 --- a/apps/client/src/widgets/FloatingButtonsDefinitions.tsx +++ b/apps/client/src/widgets/FloatingButtonsDefinitions.tsx @@ -5,7 +5,7 @@ import NoteContext from "../components/note_context"; import FNote from "../entities/fnote"; import ActionButton, { ActionButtonProps } from "./react/ActionButton"; import { useIsNoteReadOnly, useNoteLabelBoolean, useTriliumEvent, useTriliumOption, useWindowSize } from "./react/hooks"; -import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks"; import { createImageSrcUrl, openInAppHelpFromUrl } from "../services/utils"; import server from "../services/server"; import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons"; @@ -20,6 +20,7 @@ import RawHtml from "./react/RawHtml"; import { ViewTypeOptions } from "./collections/interface"; import attributes from "../services/attributes"; import LoadResults from "../services/load_results"; +import { isExperimentalFeatureEnabled } from "../services/experimental_features"; export interface FloatingButtonContext { parentComponent: Component; @@ -76,6 +77,8 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [ ToggleReadOnlyButton ]; +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); + function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) { const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode; return isEnabled && (null); - - function refresh() { - if (!isDefaultViewMode) return; - - server.get(`note-map/${note.noteId}/backlink-count`).then(resp => { - setBacklinkCount(resp.count); - }); - } - - useEffect(() => refresh(), [ note ]); - useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - if (needsRefresh(note, loadResults)) refresh(); - }); + const backlinkCount = useBacklinkCount(note, isDefaultViewMode); // Determine the max height of the container. const { windowHeight } = useWindowSize(); @@ -336,7 +326,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) { } }, [ popupOpen, windowHeight ]); - const isEnabled = isDefaultViewMode && backlinkCount > 0; + const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0; return (isEnabled &&
{ + if (!note || !isDefaultViewMode) return; + + server.get(`note-map/${note.noteId}/backlink-count`).then(resp => { + setBacklinkCount(resp.count); + }); + }, [ isDefaultViewMode, note ]); + + useEffect(() => refresh(), [ refresh ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + if (note && needsRefresh(note, loadResults)) refresh(); + }); + + return backlinkCount; +} + +export function BacklinksList({ note }: { note: FNote }) { const [ backlinks, setBacklinks ] = useState([]); function refresh() { server.get(`note-map/${note.noteId}/backlinks`).then(async (backlinks) => { // prefetch all const noteIds = backlinks - .filter(bl => "noteId" in bl) - .map((bl) => bl.noteId); + .filter(bl => "noteId" in bl) + .map((bl) => bl.noteId); await froca.getNotes(noteIds); setBacklinks(backlinks); }); diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/NoteTitleDetails.tsx index 793a3a593..58f4da0f7 100644 --- a/apps/client/src/widgets/NoteTitleDetails.tsx +++ b/apps/client/src/widgets/NoteTitleDetails.tsx @@ -1,23 +1,60 @@ -import { t } from "../services/i18n"; +import { type ComponentChild } from "preact"; + import { formatDateTime } from "../utils/formatters"; -import { useNoteContext } from "./react/hooks"; +import { useNoteContext, useStaticTooltip } from "./react/hooks"; import { joinElements } from "./react/react_utils"; import { useNoteMetadata } from "./ribbon/NoteInfoTab"; +import { Trans } from "react-i18next"; +import { useRef } from "preact/hooks"; export default function NoteTitleDetails() { - const { note } = useNoteContext(); + const { note, noteContext } = useNoteContext(); const { metadata } = useNoteMetadata(note); + const isHiddenNote = note?.noteId.startsWith("_"); + const isDefaultView = noteContext?.viewScope?.viewMode === "default"; + + const items: ComponentChild[] = [ + (isDefaultView && !isHiddenNote && metadata?.dateCreated && + ), + (isDefaultView && !isHiddenNote && metadata?.dateModified && + ) + ].filter(item => !!item); return (
- {joinElements([ - metadata?.dateCreated &&
  • - {t("note_title.created_on", { date: formatDateTime(metadata.dateCreated, "medium", "none")} )} -
  • , - metadata?.dateModified &&
  • - {t("note_title.last_modified", { date: formatDateTime(metadata.dateModified, "medium", "none")} )} -
  • - ], " • ")} + {joinElements(items, " • ")}
    ); } + +function TextWithValue({ i18nKey, value, valueTooltip }: { + i18nKey: string; + value: string; + valueTooltip: string; +}) { + const listItemRef = useRef(null); + useStaticTooltip(listItemRef, { + selector: "span.value", + title: valueTooltip, + popperConfig: { placement: "bottom" } + }); + + return ( +
  • + {value} as React.ReactElement + }} + /> +
  • + ); +} diff --git a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts index 2a7a55aef..8ae3ae674 100644 --- a/apps/client/src/widgets/attribute_widgets/attribute_detail.ts +++ b/apps/client/src/widgets/attribute_widgets/attribute_detail.ts @@ -12,6 +12,7 @@ import shortcutService from "../../services/shortcuts.js"; import appContext from "../../components/app_context.js"; import type { Attribute } from "../../services/attribute_parser.js"; import { focusSavedElement, saveFocusedElement } from "../../services/focus.js"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js"; const TPL = /*html*/`
    @@ -309,6 +310,8 @@ interface SearchRelatedResponse { count: number; } +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); + export default class AttributeDetailWidget extends NoteContextAwareWidget { private $title!: JQuery; private $inputName!: JQuery; @@ -579,6 +582,13 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget { .css("top", y - offset.top + 70) .css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000); + if (isNewLayout) { + this.$widget + .css("top", "unset") + .css("bottom", 70) + .css("max-height", "80vh"); + } + if (focus === "name") { this.$inputName.trigger("focus").trigger("select"); } diff --git a/apps/client/src/widgets/collections/board/index.tsx b/apps/client/src/widgets/collections/board/index.tsx index a50213a31..39e715f97 100644 --- a/apps/client/src/widgets/collections/board/index.tsx +++ b/apps/client/src/widgets/collections/board/index.tsx @@ -243,7 +243,7 @@ function AddNewColumn({ api, isInRelationMode }: { api: BoardApi, isInRelationMo export function TitleEditor({ currentValue, placeholder, save, dismiss, mode, isNewItem }: { currentValue?: string; placeholder?: string; - save: (newValue: string) => void; + save: (newValue: string) => void | Promise; dismiss: () => void; isNewItem?: boolean; mode?: "normal" | "multiline" | "relation"; diff --git a/apps/client/src/widgets/dialogs/PopupEditor.css b/apps/client/src/widgets/dialogs/PopupEditor.css index 8d1464bed..136bc5015 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.css +++ b/apps/client/src/widgets/dialogs/PopupEditor.css @@ -31,6 +31,7 @@ body.mobile .modal.popup-editor-dialog .modal-dialog { flex-grow: 1; display: flex; align-items: center; + margin-block: 0; } .modal.popup-editor-dialog .modal-header .note-title-widget { diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx index c85dcd3b3..152afaa4a 100644 --- a/apps/client/src/widgets/dialogs/PopupEditor.tsx +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -1,26 +1,32 @@ -import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; -import Modal from "../react/Modal"; import "./PopupEditor.css"; -import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks"; -import NoteTitleWidget from "../note_title"; -import NoteIcon from "../note_icon"; -import NoteContext from "../../components/note_context"; -import { NoteContextContext, ParentComponent } from "../react/react_utils"; -import NoteDetail from "../NoteDetail"; + import { ComponentChildren } from "preact"; +import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; + +import appContext from "../../components/app_context"; +import NoteContext from "../../components/note_context"; +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 StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter"; -import FormattingToolbar from "../ribbon/FormattingToolbar"; -import PromotedAttributes from "../PromotedAttributes"; import FloatingButtons from "../FloatingButtons"; import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions"; -import utils from "../../services/utils"; -import tree from "../../services/tree"; -import froca from "../../services/froca"; +import NoteIcon from "../note_icon"; +import NoteTitleWidget from "../note_title"; +import NoteDetail from "../NoteDetail"; +import PromotedAttributes from "../PromotedAttributes"; +import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks"; +import Modal from "../react/Modal"; +import { NoteContextContext, ParentComponent } from "../react/react_utils"; 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 { t } from "../../services/i18n"; -import appContext from "../../components/app_context"; +import BreadcrumbBadges from "../BreadcrumbBadges"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; + +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); export default function PopupEditor() { const [ shown, setShown ] = useState(false); @@ -61,7 +67,10 @@ export default function PopupEditor() { } + title={<> + + {isNewLayout && } + } customTitleBarButtons={[{ iconClassName: "bx-expand-alt", title: t("popup-editor.maximize"), @@ -75,19 +84,17 @@ export default function PopupEditor() { className="popup-editor-dialog" size="lg" show={shown} - onShown={() => { - parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId }); - }} + onShown={() => parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId })} onHidden={() => setShown(false)} keepInDom // needed for faster loading noFocus // automatic focus breaks block popup > - + {!isNewLayout && } {isMobile - ? - : } + ? + : } @@ -95,7 +102,7 @@ export default function PopupEditor() { - ) + ); } export function DialogWrapper({ children }: { children: ComponentChildren }) { @@ -107,7 +114,7 @@ export function DialogWrapper({ children }: { children: ComponentChildren }) {
    {children}
    - ) + ); } export function TitleRow() { @@ -116,5 +123,5 @@ export function TitleRow() {
    - ) + ); } diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 7d39b6b02..8769c74ae 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -29,30 +29,73 @@ body.desktop .note-title-widget input.note-title { font-size: 180%; } -body.experimental-feature-new-layout .title-row, -body.experimental-feature-new-layout .title-details { - max-width: var(--max-content-width); -} +body.experimental-feature-new-layout { + .title-row, + .title-details { + max-width: var(--max-content-width); + padding: 0; + padding-inline-start: 24px; + } -body.experimental-feature-new-layout .title-row { - margin-top: 2em; - margin-left: 12px; -} + .title-row { + margin-left: 12px; -body.experimental-feature-new-layout .title-details { - margin-top: 0; - contain: none; - padding: 0; - padding-inline-start: 24px; - opacity: 0.85; - display: flex; - gap: 0.25em; - margin: 0; - list-style-type: none; - margin-bottom: 2em; -} + .note-icon-widget { + padding: 0; + width: 41px; + } + } -body.experimental-feature-new-layout.prefers-centered-content .title-row, -body.experimental-feature-new-layout.prefers-centered-content .title-details { - margin-inline: auto; + .note-split.type-code:not(.mime-text-x-sqlite) .title-row, + .note-split.type-code:not(.mime-text-x-sqlite) .title-details { + background-color: var(--main-background-color); + } + + .title-details { + margin-top: 0; + contain: none; + display: flex; + gap: 0.25em; + margin: 0; + list-style-type: none; + + span.value { + font-weight: 500; + } + } + + .note-split.view-mode-default { + .title-row { + padding-top: 2em; + box-sizing: content-box; + } + + .title-details { + padding-bottom: 2em; + } + } + + .scrolling-container:has(> :is(.note-detail.full-height, .note-list-widget.full-height)) { + .title-row, + .title-details { + width: 100%; + max-width: unset; + padding-inline-start: 15px; + } + + .title-row { + margin-top: 0; + } + + .title-details { + margin-bottom: 0.2em; + opacity: 0.65; + font-size: 0.8em; + } + } + + &.prefers-centered-content .title-row, + &.prefers-centered-content .title-details { + margin-inline: auto; + } } diff --git a/apps/client/src/widgets/note_wrapper.ts b/apps/client/src/widgets/note_wrapper.ts index f3c61859d..d743d9ffa 100644 --- a/apps/client/src/widgets/note_wrapper.ts +++ b/apps/client/src/widgets/note_wrapper.ts @@ -62,6 +62,7 @@ export default class NoteWrapperWidget extends FlexContainer { this.$widget.addClass(utils.getNoteTypeClass(note.type)); this.$widget.addClass(utils.getMimeTypeClass(note.mime)); + this.$widget.addClass(`view-mode-${this.noteContext?.viewScope?.viewMode ?? "default"}`); this.$widget.toggleClass(["bgfx", "options"], note.isOptions()); this.$widget.toggleClass("protected", note.isProtected); diff --git a/apps/client/src/widgets/react/Dropdown.tsx b/apps/client/src/widgets/react/Dropdown.tsx index 5416e38ac..dec0660c0 100644 --- a/apps/client/src/widgets/react/Dropdown.tsx +++ b/apps/client/src/widgets/react/Dropdown.tsx @@ -117,8 +117,8 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi aria-expanded="false" id={id ?? ariaId} disabled={disabled} - onMouseOver={() => showTooltip()} - onMouseLeave={() => hideTooltip()} + onMouseEnter={showTooltip} + onMouseLeave={hideTooltip} {...buttonProps} > {text} diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index 5faab055f..0eb6108b8 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -161,11 +161,16 @@ export function FormDropdownDivider() { return
    ; } -export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) { +export function FormDropdownSubmenu({ icon, title, children, dropStart }: { + icon: string, + title: ComponentChildren, + children: ComponentChildren, + dropStart?: boolean +}) { const [ openOnMobile, setOpenOnMobile ] = useState(false); return ( -
  • +
  • { @@ -184,5 +189,5 @@ export function FormDropdownSubmenu({ icon, title, children }: { icon: string, t {children}
  • - ) + ); } diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index fbcd7095e..38f0a1967 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -201,7 +201,7 @@ export function useTriliumOptionBool(name: OptionNames, needsRefresh?: boolean): return [ (value === "true"), (newValue) => setValue(newValue ? "true" : "false") - ] + ]; } /** @@ -217,17 +217,18 @@ export function useTriliumOptionInt(name: OptionNames): [number, (newValue: numb return [ (parseInt(value, 10)), (newValue) => setValue(newValue) - ] + ]; } /** * Similar to {@link useTriliumOption}, but the object value is parsed to and from a JSON instead of a string. * * @param name the name of the option to listen for. + * @param needsRefresh whether to reload the frontend whenever the value is changed. * @returns an array where the first value is the current option value and the second value is the setter. */ -export function useTriliumOptionJson(name: OptionNames): [ T, (newValue: T) => Promise ] { - const [ value, setValue ] = useTriliumOption(name); +export function useTriliumOptionJson(name: OptionNames, needsRefresh?: boolean): [ T, (newValue: T) => Promise ] { + const [ value, setValue ] = useTriliumOption(name, needsRefresh); useDebugValue(name); return [ (JSON.parse(value) as T), @@ -845,9 +846,9 @@ export function useGlobalShortcut(keyboardShortcut: string | null | undefined, h export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) { const [ isReadOnly, setIsReadOnly ] = useState(undefined); - const enableEditing = useCallback(() => { + const enableEditing = useCallback((enabled = true) => { if (noteContext?.viewScope) { - noteContext.viewScope.readOnlyTemporarilyDisabled = true; + noteContext.viewScope.readOnlyTemporarilyDisabled = enabled; appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext}); } }, [noteContext]); @@ -862,7 +863,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { if (noteContext?.ntxId === eventNoteContext.ntxId) { - setIsReadOnly(false); + setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled); } }); diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index 6adaf7f61..12655262e 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -13,7 +13,7 @@ import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/u import ws from "../../services/ws"; import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; -import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList"; +import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList"; import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteProperty, useTriliumOption } from "../react/hooks"; import { ParentComponent } from "../react/react_utils"; import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; @@ -98,7 +98,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not } - + - + window.open(`/?print=#root/${note.noteId}`, "_blank")} >Open print page - {note.type === "text" && ( - { - noteContext?.getTextEditor(editor => { - editor.editing.view.change(() => { - throw new Error("Editor crashed."); - }); + { + noteContext?.getTextEditor(editor => { + editor.editing.view.change(() => { + throw new Error("Editor crashed."); }); - }}>Crash editor)} - + }); + }}>Crash editor + ); } diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 280f7cdda..ab351637d 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -22,7 +22,7 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ { title: t("classic_editor_toolbar.title"), icon: "bx bx-text", - show: async ({ note, noteContext }) => note?.type === "text" + show: async ({ note, noteContext }) => note?.type === "text" && noteContext?.viewScope?.viewMode === "default" && options.get("textNoteEditorType") === "ckeditor-classic" && !(await noteContext?.isReadOnly()), toggleCommand: "toggleRibbonTabClassicEditor", diff --git a/apps/client/src/widgets/shared_info.tsx b/apps/client/src/widgets/shared_info.tsx index 954ceb5f0..cd6cf78f4 100644 --- a/apps/client/src/widgets/shared_info.tsx +++ b/apps/client/src/widgets/shared_info.tsx @@ -26,6 +26,7 @@ export default function SharedInfo() { export function useShareInfo(note: FNote | null | undefined) { const [ link, setLink ] = useState(); + const [ linkHref, setLinkHref ] = useState(); const [ syncServerHost ] = useTriliumOption("syncServerHost"); function refresh() { @@ -52,9 +53,10 @@ export function useShareInfo(note: FNote | null | undefined) { } setLink(`${link}`); + setLinkHref(link); } - useEffect(refresh, [ note ]); + useEffect(refresh, [ note, syncServerHost ]); useTriliumEvent("entitiesReloaded", ({ loadResults }) => { if (loadResults.getAttributeRows().find((attr) => attr.name?.startsWith("_share") && attributes.isAffecting(attr, note))) { refresh(); @@ -63,7 +65,7 @@ export function useShareInfo(note: FNote | null | undefined) { } }); - return { link, isSharedExternally: !!syncServerHost }; + return { link, linkHref, isSharedExternally: !!syncServerHost }; } function getShareId(note: FNote) { diff --git a/apps/client/src/widgets/type_widgets/options/advanced.tsx b/apps/client/src/widgets/type_widgets/options/advanced.tsx index 958180063..4024a9d0e 100644 --- a/apps/client/src/widgets/type_widgets/options/advanced.tsx +++ b/apps/client/src/widgets/type_widgets/options/advanced.tsx @@ -158,7 +158,7 @@ function ExistingAnonymizedDatabases({ databases }: { databases: AnonymizedDbRes ))} - ) + ); } function VacuumDatabaseOptions() { @@ -175,11 +175,11 @@ function VacuumDatabaseOptions() { }} /> - ) + ); } function ExperimentalOptions() { - const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson("experimentalFeatures"); + const [ enabledExperimentalFeatures, setEnabledExperimentalFeatures ] = useTriliumOptionJson("experimentalFeatures", true); return (