diff --git a/apps/client/src/components/root_command_executor.ts b/apps/client/src/components/root_command_executor.ts index 918809cd1e..048aa4bd09 100644 --- a/apps/client/src/components/root_command_executor.ts +++ b/apps/client/src/components/root_command_executor.ts @@ -1,14 +1,14 @@ -import Component from "./component.js"; -import appContext, { type CommandData, type CommandListenerData } from "./app_context.js"; import dateNoteService from "../services/date_notes.js"; -import treeService from "../services/tree.js"; -import openService from "../services/open.js"; -import protectedSessionService from "../services/protected_session.js"; -import options from "../services/options.js"; import froca from "../services/froca.js"; -import utils, { openInReusableSplit } from "../services/utils.js"; -import toastService from "../services/toast.js"; import noteCreateService from "../services/note_create.js"; +import openService from "../services/open.js"; +import options from "../services/options.js"; +import protectedSessionService from "../services/protected_session.js"; +import toastService from "../services/toast.js"; +import treeService from "../services/tree.js"; +import utils, { openInReusableSplit } from "../services/utils.js"; +import appContext, { type CommandListenerData } from "./app_context.js"; +import Component from "./component.js"; export default class RootCommandExecutor extends Component { editReadOnlyNoteCommand() { @@ -193,10 +193,13 @@ export default class RootCommandExecutor extends Component { appContext.triggerEvent("zenModeChanged", { isEnabled }); } - async toggleRibbonTabNoteMapCommand() { + async toggleRibbonTabNoteMapCommand(data: CommandListenerData<"toggleRibbonTabNoteMap">) { const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js"); const isNewLayout = isExperimentalFeatureEnabled("new-layout"); - if (!isNewLayout) return; + if (!isNewLayout) { + this.triggerEvent("toggleRibbonTabNoteMap", data); + return; + } const activeContext = appContext.tabManager.getActiveContext(); if (!activeContext?.notePath) return; @@ -272,7 +275,7 @@ export default class RootCommandExecutor extends Component { } catch (e) { console.error("Error creating AI Chat note:", e); - toastService.showError("Failed to create AI Chat note: " + (e as Error).message); + toastService.showError(`Failed to create AI Chat note: ${(e as Error).message}`); } } } diff --git a/apps/client/src/entities/fnote.ts b/apps/client/src/entities/fnote.ts index 0f6df098a5..cd1a8b7a7a 100644 --- a/apps/client/src/entities/fnote.ts +++ b/apps/client/src/entities/fnote.ts @@ -1,17 +1,17 @@ -import server from "../services/server.js"; -import noteAttributeCache from "../services/note_attribute_cache.js"; -import protectedSessionHolder from "../services/protected_session_holder.js"; import cssClassManager from "../services/css_class_manager.js"; import type { Froca } from "../services/froca-interface.js"; -import type FAttachment from "./fattachment.js"; -import type { default as FAttribute, AttributeType } from "./fattribute.js"; -import utils from "../services/utils.js"; +import noteAttributeCache from "../services/note_attribute_cache.js"; +import protectedSessionHolder from "../services/protected_session_holder.js"; import search from "../services/search.js"; +import server from "../services/server.js"; +import utils from "../services/utils.js"; +import type FAttachment from "./fattachment.js"; +import type { AttributeType,default as FAttribute } from "./fattribute.js"; const LABEL = "label"; const RELATION = "relation"; -const NOTE_TYPE_ICONS = { +export const NOTE_TYPE_ICONS = { file: "bx bx-file", image: "bx bx-image", code: "bx bx-code", @@ -268,13 +268,12 @@ export default class FNote { } } return results; - } else { - return this.children; } + return this.children; } async getSubtreeNoteIds(includeArchived = false) { - let noteIds: (string | string[])[] = []; + const noteIds: (string | string[])[] = []; for (const child of await this.getChildNotes()) { if (child.isArchived && !includeArchived) continue; @@ -471,9 +470,8 @@ export default class FNote { return a.isHidden ? 1 : -1; } else if (a.isSearch !== b.isSearch) { return a.isSearch ? 1 : -1; - } else { - return a.notePath.length - b.notePath.length; } + return a.notePath.length - b.notePath.length; }); return notePaths; @@ -597,14 +595,12 @@ export default class FNote { } else if (this.type === "text") { if (this.isFolder()) { return "bx bx-folder"; - } else { - return "bx bx-note"; } + return "bx bx-note"; } else if (this.type === "code" && this.mime.startsWith("text/x-sql")) { return "bx bx-data"; - } else { - return NOTE_TYPE_ICONS[this.type]; } + return NOTE_TYPE_ICONS[this.type]; } getColorClass() { @@ -617,7 +613,7 @@ export default class FNote { } getFilteredChildBranches() { - let childBranches = this.getChildBranches(); + const childBranches = this.getChildBranches(); if (!childBranches) { console.error(`No children for '${this.noteId}'. This shouldn't happen.`); @@ -811,9 +807,9 @@ export default class FNote { return this.getLabelValue(nameWithPrefix.substring(1)); } else if (nameWithPrefix.startsWith("~")) { return this.getRelationValue(nameWithPrefix.substring(1)); - } else { - return this.getLabelValue(nameWithPrefix); } + return this.getLabelValue(nameWithPrefix); + } /** @@ -878,10 +874,10 @@ export default class FNote { promotedAttrs.sort((a, b) => { if (a.noteId === b.noteId) { return a.position < b.position ? -1 : 1; - } else { - // inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 - return a.noteId < b.noteId ? -1 : 1; } + // inherited promoted attributes should stay grouped: https://github.com/zadam/trilium/issues/3761 + return a.noteId < b.noteId ? -1 : 1; + }); return promotedAttrs; diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts index 3984674102..d252e2a776 100644 --- a/apps/client/src/services/experimental_features.ts +++ b/apps/client/src/services/experimental_features.ts @@ -20,11 +20,19 @@ export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"]; let enabledFeatures: Set | null = null; export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean { + if (featureId === "new-layout") { + return options.is("newLayout"); + } + return getEnabledFeatures().has(featureId); } export function getEnabledExperimentalFeatureIds() { - return getEnabledFeatures().values(); + const values = [ ...getEnabledFeatures().values() ]; + if (options.is("newLayout")) { + values.push("new-layout"); + } + return values; } export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) { diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index a7a13f2882..f10ace6947 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -717,12 +717,17 @@ table.promoted-attributes-in-tooltip th { .tooltip { font-size: var(--main-font-size) !important; z-index: calc(var(--ck-z-panel) - 1) !important; + white-space: pre-wrap; } .tooltip.tooltip-top { z-index: 32767 !important; } +.pre-wrap-text { + white-space: pre-wrap; +} + .bs-tooltip-bottom .tooltip-arrow::before { border-bottom-color: var(--main-border-color) !important; } diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 8764f80018..19c2940445 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -2109,6 +2109,8 @@ "background_effects_title": "Background effects are now stable", "background_effects_message": "On Windows devices, background effects are now fully stable. The background effects adds a touch of color to the user interface by blurring the background behind it. This technique is also used in other applications such as Windows Explorer.", "background_effects_button": "Enable background effects", + "new_layout_title": "New layout", + "new_layout_message": "We’ve introduced a modernized layout for Trilium. The ribbon has been removed and seamlessly integrated into the main interface, with a new status bar and expandable sections (such as promoted attributes) taking over key functions.\n\nThe new layout is enabled by default, and can be temporarily disabled via Options → Appearance.", "dismiss": "Dismiss" }, "settings": { @@ -2116,7 +2118,10 @@ }, "settings_appearance": { "related_code_blocks": "Color scheme for code blocks in text notes", - "related_code_notes": "Color scheme for code notes" + "related_code_notes": "Color scheme for code notes", + "ui": "User interface", + "ui_old_layout": "Old layout", + "ui_new_layout": "New layout" }, "units": { "percentage": "%" diff --git a/apps/client/src/widgets/buttons/global_menu.tsx b/apps/client/src/widgets/buttons/global_menu.tsx index fa24c8387f..255cf89c9f 100644 --- a/apps/client/src/widgets/buttons/global_menu.tsx +++ b/apps/client/src/widgets/buttons/global_menu.tsx @@ -1,7 +1,7 @@ import "./global_menu.css"; import { KeyboardActionNames } from "@triliumnext/commons"; -import { ComponentChildren } from "preact"; +import { ComponentChildren, RefObject } from "preact"; import { useContext, useEffect, useRef, useState } from "preact/hooks"; import { CommandNames } from "../../components/app_context"; @@ -30,13 +30,15 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: const parentComponent = useContext(ParentComponent); const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus(); const isMobileLocal = isMobile(); + const logoRef = useRef(null); + useStaticTooltip(logoRef); return ( - {isVerticalLayout && } + {isVerticalLayout && } {isUpdateAvailable && } @@ -135,9 +137,9 @@ function SwitchToOptions() { return; } else if (!isMobile()) { return ; - } + } return ; - + } function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps void)>) { @@ -159,10 +161,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps; } -function VerticalLayoutIcon() { - const logoRef = useRef(null); - useStaticTooltip(logoRef); - +export function VerticalLayoutIcon({ logoRef }: { logoRef?: RefObject }) { return ( diff --git a/apps/client/src/widgets/dialogs/call_to_action.tsx b/apps/client/src/widgets/dialogs/call_to_action.tsx index 1ae4a14ebb..4f6da5293c 100644 --- a/apps/client/src/widgets/dialogs/call_to_action.tsx +++ b/apps/client/src/widgets/dialogs/call_to_action.tsx @@ -1,11 +1,12 @@ import { useMemo, useState } from "preact/hooks"; + +import { t } from "../../services/i18n"; import Button from "../react/Button"; import Modal from "../react/Modal"; import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions"; -import { t } from "../../services/i18n"; export default function CallToActionDialog() { - const activeCallToActions = useMemo(() => getCallToActions(), []); + const activeCallToActions = useMemo(() => getCallToActions(), []); const [ activeIndex, setActiveIndex ] = useState(0); const [ shown, setShown ] = useState(true); const activeItem = activeCallToActions[activeIndex]; @@ -36,11 +37,11 @@ export default function CallToActionDialog() { await dismissCallToAction(activeItem.id); await button.onClick(); goToNext(); - }}/> + }}/> )} } > -

{activeItem.message}

+

{activeItem.message}

- ) + ); } diff --git a/apps/client/src/widgets/dialogs/call_to_action_definitions.ts b/apps/client/src/widgets/dialogs/call_to_action_definitions.ts index 31982689e4..056672b169 100644 --- a/apps/client/src/widgets/dialogs/call_to_action_definitions.ts +++ b/apps/client/src/widgets/dialogs/call_to_action_definitions.ts @@ -1,6 +1,6 @@ -import utils from "../../services/utils"; -import options from "../../services/options"; import { t } from "../../services/i18n"; +import options from "../../services/options"; +import utils from "../../services/utils"; /** * A "call-to-action" is an interactive message for the user, generally to present new features. @@ -46,20 +46,11 @@ function isNextTheme() { const CALL_TO_ACTIONS: CallToAction[] = [ { - id: "next_theme", - title: t("call_to_action.next_theme_title"), - message: t("call_to_action.next_theme_message"), - enabled: () => !isNextTheme(), - buttons: [ - { - text: t("call_to_action.next_theme_button"), - async onClick() { - await options.save("theme", "next"); - await options.save("backgroundEffects", "true"); - utils.reloadFrontendApp("call-to-action"); - } - } - ] + id: "new_layout", + title: t("call_to_action.new_layout_title"), + message: t("call_to_action.new_layout_message"), + enabled: () => true, + buttons: [] }, { id: "background_effects", @@ -75,6 +66,22 @@ const CALL_TO_ACTIONS: CallToAction[] = [ } } ] + }, + { + id: "next_theme", + title: t("call_to_action.next_theme_title"), + message: t("call_to_action.next_theme_message"), + enabled: () => !isNextTheme(), + buttons: [ + { + text: t("call_to_action.next_theme_button"), + async onClick() { + await options.save("theme", "next"); + await options.save("backgroundEffects", "true"); + utils.reloadFrontendApp("call-to-action"); + } + } + ] } ]; diff --git a/apps/client/src/widgets/layout/InlineTitle.css b/apps/client/src/widgets/layout/InlineTitle.css index a667e35fc5..613c845e3c 100644 --- a/apps/client/src/widgets/layout/InlineTitle.css +++ b/apps/client/src/widgets/layout/InlineTitle.css @@ -7,7 +7,6 @@ } .inline-title { - margin-top: 2px; /* Allow space for the focus outline */ max-width: var(--max-content-width); container-type: inline-size; padding-inline-start: 24px; @@ -111,7 +110,7 @@ body.prefers-centered-content .inline-title { .note-type-switcher { --badge-radius: 12px; - + position: relative; top: 5px; padding: .25em 0; @@ -121,7 +120,7 @@ body.prefers-centered-content .inline-title { min-width: 0; gap: 5px; min-height: 35px; - + >* { flex-shrink: 0; animation: note-type-switcher-intro 200ms ease-in; diff --git a/apps/client/src/widgets/layout/NoteBadges.tsx b/apps/client/src/widgets/layout/NoteBadges.tsx index 9cbbf6fcc3..245f231999 100644 --- a/apps/client/src/widgets/layout/NoteBadges.tsx +++ b/apps/client/src/widgets/layout/NoteBadges.tsx @@ -22,11 +22,10 @@ export default function NoteBadges() { function ReadOnlyBadge() { const { note, noteContext } = useNoteContext(); - const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); + const { isReadOnly, enableEditing, temporarilyEditable } = useIsNoteReadOnly(note, noteContext); const isExplicitReadOnly = note?.isLabelTruthy("readOnly"); - const isTemporarilyEditable = noteContext?.ntxId !== "_popup-editor" && noteContext?.viewScope?.readOnlyTemporarilyDisabled; - if (isTemporarilyEditable) { + if (temporarilyEditable) { return setExpanded(!expanded)); + if (!cells?.length) return false; return (note && ( void; } -export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) { +export function NoteInfoBadge({ note, similarNotesShown, setSimilarNotesShown }: NoteInfoContext) { const dropdownRef = useRef(null); const { metadata, ...sizeProps } = useNoteMetadata(note); const [ originalFileName ] = useNoteLabel(note, "originalFileName"); - const currentNoteType = useNoteProperty(note, "type"); - const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]); + const noteType = useNoteProperty(note, "type"); + const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]); + const enabled = note && noteType && noteTypeMapping; - return (note && currentNoteTypeData && + // Keyboard shortcut. + useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show()); + useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown)); + + return (enabled && } - {" "}{currentNoteTypeData?.title}} /> + {" "}{noteTypeMapping?.title}} /> {note.mime && } {note.noteId}} /> } /> @@ -349,6 +354,10 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown // Show on keyboard shortcuts. useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true)); + useTriliumEvents([ "toggleRibbonTabOwnedAttributes", "toggleRibbonTabInheritedAttributes" ], () => setAttributesShown(!attributesShown)); + + // Auto-focus the owned attributes. + useEffect(() => api.current?.focus(), [ attributesShown ]); // Interaction with the attribute editor. useLegacyImperativeHandlers(useMemo(() => ({ @@ -373,12 +382,18 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown //#region Note paths function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) { + const dropdownRef = useRef(null); const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId); const count = sortedNotePaths?.length ?? 0; + const enabled = count > 1; - return (count > 1 && + // Keyboard shortcut. + useTriliumEvent("toggleRibbonTabNotePaths", () => enabled && dropdownRef.current?.show()); + + return (enabled && void }) { + // Keyboard shortcut + const dropdownContainerRef = useRef(null); + useTriliumEvent("toggleRibbonTabBookProperties", () => { + dropdownContainerRef.current?.querySelector("button")?.focus(); + }); + return (   {VIEW_TYPE_MAPPINGS[viewType]} diff --git a/apps/client/src/widgets/react/FormList.tsx b/apps/client/src/widgets/react/FormList.tsx index dd40bd08cc..84cc4d8c65 100644 --- a/apps/client/src/widgets/react/FormList.tsx +++ b/apps/client/src/widgets/react/FormList.tsx @@ -2,13 +2,13 @@ import "./FormList.css"; import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap"; import clsx from "clsx"; -import { ComponentChildren } from "preact"; +import { ComponentChildren, RefObject } from "preact"; import { type CSSProperties,useEffect, useMemo, useRef, useState } from "preact/compat"; import { CommandNames } from "../../components/app_context"; import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils"; import FormToggle from "./FormToggle"; -import { useStaticTooltip } from "./hooks"; +import { useStaticTooltip, useSyncedRef } from "./hooks"; import Icon from "./Icon"; interface FormListOpts { @@ -97,6 +97,7 @@ interface FormListItemOpts { className?: string; rtl?: boolean; postContent?: ComponentChildren; + itemRef?: RefObject; } const TOOLTIP_CONFIG: Partial = { @@ -104,8 +105,8 @@ const TOOLTIP_CONFIG: Partial = { fallbackPlacements: [ handleRightToLeftPlacement("right") ] }; -export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) { - const itemRef = useRef(null); +export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, itemRef: externalItemRef, ...contentProps }: FormListItemOpts) { + const itemRef = useSyncedRef(externalItemRef, null); if (checked) { icon = "bx bx-check"; diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index f23b85d0cc..e43ee4c8de 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -933,11 +933,13 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N const [ isReadOnly, setIsReadOnly ] = useState(undefined); const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly"); const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled"); + const [ temporarilyEditable, setTemporarilyEditable ] = useState(false); const enableEditing = useCallback((enabled = true) => { if (noteContext?.viewScope) { noteContext.viewScope.readOnlyTemporarilyDisabled = enabled; appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext}); + setTemporarilyEditable(enabled); } }, [noteContext]); @@ -945,6 +947,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N if (note && noteContext) { isNoteReadOnly(note, noteContext).then((readOnly) => { setIsReadOnly(readOnly); + setTemporarilyEditable(false); }); } }, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]); @@ -952,10 +955,11 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { if (noteContext?.ntxId === eventNoteContext.ntxId) { setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled); + setTemporarilyEditable(true); } }); - return { isReadOnly, enableEditing }; + return { isReadOnly, enableEditing, temporarilyEditable }; } async function isNoteReadOnly(note: FNote, noteContext: NoteContext) { diff --git a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx index 610063a3c0..0225a74bcf 100644 --- a/apps/client/src/widgets/ribbon/FormattingToolbar.tsx +++ b/apps/client/src/widgets/ribbon/FormattingToolbar.tsx @@ -49,6 +49,21 @@ export function FixedFormattingToolbar() { const renderState = useRenderState(noteContext, note); const [ toolbarToRender, setToolbarToRender ] = useState(); + // Keyboard shortcut. + const lastFocusedElement = useRef(null); + useTriliumEvent("toggleRibbonTabClassicEditor", () => { + if (!toolbarToRender) return; + if (!toolbarToRender.contains(document.activeElement)) { + // Focus to the fixed formatting toolbar. + lastFocusedElement.current = document.activeElement; + toolbarToRender.querySelector(".ck-toolbar__items button")?.focus(); + } else { + // Focus back to the last selection. + (lastFocusedElement.current as HTMLElement)?.focus(); + lastFocusedElement.current = null; + } + }); + // Populate the cache with the toolbar of every note context. useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => { if (!eventNtxId) return; diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index bb6d9a8d8b..dfb0d93bbb 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -1,5 +1,7 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons"; -import { useContext } from "preact/hooks"; +import { Dropdown as BootstrapDropdown } from "bootstrap"; +import { RefObject } from "preact"; +import { useContext, useEffect, useRef } from "preact/hooks"; import appContext, { CommandNames } from "../../components/app_context"; import Component from "../../components/component"; @@ -20,7 +22,7 @@ import MovePaneButton from "../buttons/move_pane_button"; import ActionButton from "../react/ActionButton"; import Dropdown from "../react/Dropdown"; import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList"; -import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumOption } from "../react/hooks"; +import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks"; import { ParentComponent } from "../react/react_utils"; import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab"; import NoteActionsCustom from "./NoteActionsCustom"; @@ -59,7 +61,10 @@ function RevisionsButton({ note }: { note: FNote }) { ); } +type ItemToFocus = "basic-properties"; + function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { + const dropdownRef = useRef(null); const parentComponent = useContext(ParentComponent); const noteType = useNoteProperty(note, "type") ?? ""; const [viewType] = useNoteLabel(note, "viewType"); @@ -77,14 +82,25 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not const [syncServerHost] = useTriliumOption("syncServerHost"); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); const isNormalViewMode = noteContext?.viewScope?.viewMode === "default"; + const itemToFocusRef = useRef(null); + + // Keyboard shortcuts. + useTriliumEvent("toggleRibbonTabBasicProperties", () => { + if (!isNewLayout) return; + itemToFocusRef.current = "basic-properties"; + dropdownRef.current?.toggle(); + }); return ( + iconAction + onHidden={() => itemToFocusRef.current = null } + > {isReadOnly && <> {isNewLayout && isNormalViewMode && !isHelpPage && <> - + } @@ -148,12 +164,22 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not ); } -function NoteBasicProperties({ note }: { note: FNote }) { +function NoteBasicProperties({ note, focus }: { + note: FNote; + focus: RefObject; +}) { + const itemToFocusRef = useRef(null); const [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note); const [ isShared, switchShareState ] = useShareState(note); const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template"); const isProtected = useNoteProperty(note, "isProtected"); + useEffect(() => { + if (focus.current === "basic-properties") { + itemToFocusRef.current?.focus(); + } + }, [ focus ]); + return <> (null); const noteType = useNoteProperty(note, "type"); const noteMime = useNoteProperty(note, "mime"); const [ viewType ] = useNoteLabel(note, "viewType"); @@ -53,8 +54,15 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) { isReadOnly }; + useTriliumEvents([ "toggleRibbonTabFileProperties", "toggleRibbonTabImageProperties" ], () => { + (containerRef.current?.firstElementChild as HTMLElement)?.focus(); + }); + return (innerProps && -
+
diff --git a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx index bf0aa9428e..ee9129bbd7 100644 --- a/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx +++ b/apps/client/src/widgets/ribbon/components/AttributeEditor.tsx @@ -1,25 +1,26 @@ -import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks"; import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5"; +import { AttributeType } from "@triliumnext/commons"; +import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks"; + +import type { CommandData, FilteredCommandNames } from "../../../components/app_context"; +import FAttribute from "../../../entities/fattribute"; +import FNote from "../../../entities/fnote"; +import contextMenu from "../../../menus/context_menu"; +import attribute_parser, { Attribute } from "../../../services/attribute_parser"; +import attribute_renderer from "../../../services/attribute_renderer"; +import attributes from "../../../services/attributes"; +import froca from "../../../services/froca"; import { t } from "../../../services/i18n"; -import server from "../../../services/server"; +import link from "../../../services/link"; import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete"; +import note_create from "../../../services/note_create"; +import server from "../../../services/server"; +import { isIMEComposing } from "../../../services/shortcuts"; +import { escapeQuotes, getErrorMessage } from "../../../services/utils"; +import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; +import ActionButton from "../../react/ActionButton"; import CKEditor, { CKEditorApi } from "../../react/CKEditor"; import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks"; -import FAttribute from "../../../entities/fattribute"; -import attribute_renderer from "../../../services/attribute_renderer"; -import FNote from "../../../entities/fnote"; -import AttributeDetailWidget from "../../attribute_widgets/attribute_detail"; -import attribute_parser, { Attribute } from "../../../services/attribute_parser"; -import ActionButton from "../../react/ActionButton"; -import { escapeQuotes, getErrorMessage } from "../../../services/utils"; -import link from "../../../services/link"; -import { isIMEComposing } from "../../../services/shortcuts"; -import froca from "../../../services/froca"; -import contextMenu from "../../../menus/context_menu"; -import type { CommandData, FilteredCommandNames } from "../../../components/app_context"; -import { AttributeType } from "@triliumnext/commons"; -import attributes from "../../../services/attributes"; -import note_create from "../../../services/note_create"; type AttributeCommandNames = FilteredCommandNames; @@ -52,7 +53,7 @@ const mentionSetup: MentionFeed[] = [ return names.map((name) => { return { id: `#${name}`, - name: name + name }; }); }, @@ -66,7 +67,7 @@ const mentionSetup: MentionFeed[] = [ return names.map((name) => { return { id: `~${name}`, - name: name + name }; }); }, @@ -85,9 +86,10 @@ interface AttributeEditorProps { } export interface AttributeEditorImperativeHandlers { - save: () => Promise; - refresh: () => void; - renderOwnedAttributes: (ownedAttributes: FAttribute[]) => Promise; + save(): Promise; + refresh(): void; + focus(): void; + renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise; } export default function AttributeEditor({ api, note, componentId, notePath, ntxId, hidden }: AttributeEditorProps) { @@ -124,7 +126,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI // attrs are not resorted if position changes after the initial load ownedAttributes.sort((a, b) => a.position - b.position); - let htmlAttrs = ("

" + (await attribute_renderer.renderAttributes(ownedAttributes, true)).html() + "

"); + let htmlAttrs = (`

${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}

`); if (saved) { lastSavedContent.current = htmlAttrs; @@ -162,7 +164,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI wrapperRef.current.style.opacity = "0"; setTimeout(() => { if (wrapperRef.current) { - wrapperRef.current.style.opacity = "1" + wrapperRef.current.style.opacity = "1"; } }, 100); } @@ -252,7 +254,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI if (notePath) { result = await note_create.createNoteWithTypePrompt(notePath, { activate: false, - title: title + title }); } @@ -274,7 +276,8 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI useImperativeHandle(api, () => ({ save, refresh, - renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false) + renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false), + focus: () => editorRef.current?.focus() }), [ save, refresh, renderOwnedAttributes ]); return ( @@ -404,7 +407,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI {attributeDetailWidgetEl} - ) + ); } function getPreprocessedData(currentValue: string) { diff --git a/apps/client/src/widgets/type_widgets/options/appearance.css b/apps/client/src/widgets/type_widgets/options/appearance.css new file mode 100644 index 0000000000..04643120d9 --- /dev/null +++ b/apps/client/src/widgets/type_widgets/options/appearance.css @@ -0,0 +1,114 @@ +.old-layout-illustration { + width: 170px; + height: 130px; + border: 1px solid var(--main-border-color); + border-radius: 6px; + display: flex; + background: var(--root-background); + overflow: hidden; + + .launcher-pane { + width: 10%; + background: var(--launcher-pane-vert-background-color); + display: flex; + flex-direction: column; + align-items: center; + padding: 1px 0; + + svg { + margin-top: 1px; + margin-bottom: 5px; + } + + .bx { + margin: 4px 0; + font-size: 12px; + opacity: 0.5; + } + } + + .tree { + width: 20%; + font-size: 4px; + padding: 12px 5px; + overflow: hidden; + flex-shrink: 0; + filter: blur(1px); + + ul { + list-style-type: none; + margin: 0; + padding: 0; + } + } + + .main { + display: flex; + flex-direction: column; + flex-grow: 1; + font-size: 8px; + + .tab-bar { + height: 10px; + flex-shrink: 0; + } + + .content { + background-color: var(--main-background-color); + flex-grow: 1; + border-top-left-radius: 6px; + display: flex; + flex-direction: column; + min-height: 0; + + .title-bar { + display: flex; + align-items: center; + font-size: 14px; + padding: 5px; + + .title { + flex-grow: 1; + } + } + + .ribbon { + padding: 0 5px; + + .bx { + font-size: 10px; + } + + .ribbon-header { + display: flex; + } + + .ribbon-body { + height: 20px; + background-color: rgba(0, 0, 0, 0.05); + border-radius: 6px; + margin: 1px 0; + } + } + + .content-inner { + font-size: 6px; + overflow: hidden; + padding: 5px; + opacity: 0.5; + filter: blur(1px); + } + + .status-bar { + background-color: var(--left-pane-background-color); + flex-shrink: 0; + padding: 0 2px; + display: flex; + + &> .status-bar-breadcrumb { + flex-grow: 1; + } + } + } + } +} diff --git a/apps/client/src/widgets/type_widgets/options/appearance.tsx b/apps/client/src/widgets/type_widgets/options/appearance.tsx index 809d05e9ea..feecb56f57 100644 --- a/apps/client/src/widgets/type_widgets/options/appearance.tsx +++ b/apps/client/src/widgets/type_widgets/options/appearance.tsx @@ -1,18 +1,24 @@ +import "./appearance.css"; + +import { FontFamily, OptionNames } from "@triliumnext/commons"; import { useEffect, useState } from "preact/hooks"; + import { t } from "../../../services/i18n"; -import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils"; -import Column from "../../react/Column"; -import FormRadioGroup from "../../react/FormRadioGroup"; -import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect"; -import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; -import OptionsSection from "./components/OptionsSection"; import server from "../../../services/server"; +import { isElectron, isMobile, reloadFrontendApp, restartDesktopApp } from "../../../services/utils"; +import { VerticalLayoutIcon } from "../../buttons/global_menu"; +import Button from "../../react/Button"; +import Column from "../../react/Column"; import FormCheckbox from "../../react/FormCheckbox"; import FormGroup from "../../react/FormGroup"; -import { FontFamily, OptionNames } from "@triliumnext/commons"; -import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; +import FormRadioGroup from "../../react/FormRadioGroup"; +import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect"; import FormText from "../../react/FormText"; -import Button from "../../react/Button"; +import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox"; +import { useTriliumOption, useTriliumOptionBool } from "../../react/hooks"; +import Icon from "../../react/Icon"; +import OptionsSection from "./components/OptionsSection"; +import RadioWithIllustration from "./components/RadioWithIllustration"; import RelatedSettings from "./components/RelatedSettings"; const MIN_CONTENT_WIDTH = 640; @@ -30,7 +36,7 @@ const BUILTIN_THEMES: Theme[] = [ { val: "auto", title: t("theme.auto_theme") }, { val: "light", title: t("theme.light_theme") }, { val: "dark", title: t("theme.dark_theme") } -] +]; interface FontFamilyEntry { value: FontFamily; @@ -84,6 +90,7 @@ export default function AppearanceSettings() { return (
+ {!isMobile() && } {!isMobile() && } {overrideThemeFonts === "true" && } @@ -102,7 +109,99 @@ export default function AppearanceSettings() { } ]} />
- ) + ); +} + +function LayoutSwitcher() { + const [ newLayout, setNewLayout ] = useTriliumOptionBool("newLayout"); + + return ( + + { + await setNewLayout(newValue === "new-layout"); + reloadFrontendApp(); + }} + values={[ + { key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: }, + { key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: } + ]} + /> + + ); +} + +function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) { + return ( +
+
+ + + + +
+ +
+
    +
  • Options
  • +
      +
    • Appearance
    • +
    • Shortcuts
    • +
    • Text Notes
    • +
    • Code Notes
    • +
    • Images
    • +
    +
+
+ +
+
+ +
+
+ + Title + +
+ + {!isNewLayout &&
+
+ + + + +
+ +
+
} + + {isNewLayout &&
+ {" "}Promoted attributes +
} + +
+ This is a "demo" document packaged with Trilium to showcase some of its features and also give you some ideas on how you might structure your notes. You can play with it, and modify the note content and tree structure as you wish. +
+ + {isNewLayout &&
+
+ + + Note + + Note +
+ +
+ + +
+
} +
+
+
+ ); } function LayoutOrientation() { @@ -141,7 +240,7 @@ function ApplicationTheme() { setThemes([ ...BUILTIN_THEMES, ...userThemes - ]) + ]); }); }, []); @@ -162,7 +261,7 @@ function ApplicationTheme() {
- ) + ); } function Fonts() { @@ -245,7 +344,7 @@ function ElectronIntegration() {