diff --git a/apps/client/src/layouts/desktop_layout.tsx b/apps/client/src/layouts/desktop_layout.tsx index d1f35ec62..076a76522 100644 --- a/apps/client/src/layouts/desktop_layout.tsx +++ b/apps/client/src/layouts/desktop_layout.tsx @@ -46,6 +46,12 @@ import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx"; import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx"; import Breadcrumb from "../widgets/Breadcrumb.jsx"; import TabHistoryNavigationButtons from "../widgets/TabHistoryNavigationButtons.jsx"; +import { isExperimentalFeatureEnabled } from "../services/experimental_features.js"; +import NoteActions from "../widgets/ribbon/NoteActions.jsx"; +import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.jsx"; +import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; +import BreadcrumbBadges from "../widgets/BreadcrumbBadges.jsx"; +import NoteTitleDetails from "../widgets/NoteTitleDetails.jsx"; export default class DesktopLayout { @@ -71,6 +77,12 @@ export default class DesktopLayout { */ const fullWidthTabBar = launcherPaneIsHorizontal || (isElectron && !hasNativeTitleBar && isMac); const customTitleBarButtons = !hasNativeTitleBar && !isMac && !isWindows; + const isNewLayout = isExperimentalFeatureEnabled("new-layout"); + + const titleRow = new FlexContainer("row") + .class("title-row") + .child() + .child(); const rootContainer = new RootContainer(true) .setParent(appContext) @@ -126,30 +138,27 @@ export default class DesktopLayout { .child( new FlexContainer("row") .class("breadcrumb-row") - .css("height", "30px") - .css("min-height", "30px") - .css("align-items", "center") - .css("padding", "10px") .cssBlock(".breadcrumb-row > * { margin: 5px; }") .child() + .child() .child() .child() .child() .child() .child() + .optChild(isNewLayout, ) ) - .child(new FlexContainer("row") - .class("title-row") - .child() - .child() - ) - .child() + .optChild(!isNewLayout, titleRow) + .optChild(!isNewLayout, ) + .optChild(isNewLayout, ) .child(new WatchedFileUpdateStatusWidget()) .child() .child( new ScrollingContainer() .filling() - .child(new ContentHeader() + .optChild(isNewLayout, titleRow) + .optChild(isNewLayout, ) + .optChild(!isNewLayout, new ContentHeader() .child() .child() ) @@ -167,6 +176,7 @@ export default class DesktopLayout { ...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC ...this.customWidgets.get("note-detail-pane") ) + .optChild(isNewLayout, ) ) ) .child(...this.customWidgets.get("center-pane")) diff --git a/apps/client/src/services/experimental_features.ts b/apps/client/src/services/experimental_features.ts new file mode 100644 index 000000000..76cca8461 --- /dev/null +++ b/apps/client/src/services/experimental_features.ts @@ -0,0 +1,41 @@ +import { t } from "./i18n"; +import options from "./options"; + +interface ExperimentalFeature { + id: string; + name: string; + description: string; +} + +export const experimentalFeatures = [ + { + id: "new-layout", + name: t("experimental_features.new_layout_name"), + description: t("experimental_features.new_layout_description"), + } +] as const satisfies ExperimentalFeature[]; + +type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"]; + +let enabledFeatures: Set | null = null; + +export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean { + return getEnabledFeatures().has(featureId); +} + +export function getEnabledExperimentalFeatureIds() { + return getEnabledFeatures().values(); +} + +function getEnabledFeatures() { + if (!enabledFeatures) { + let features: ExperimentalFeatureId[] = []; + try { + features = JSON.parse(options.get("experimentalFeatures")) as ExperimentalFeatureId[]; + } catch (e) { + console.warn("Failed to parse experimental features from options:", e); + } + enabledFeatures = new Set(features); + } + return enabledFeatures; +} diff --git a/apps/client/src/translations/en/translation.json b/apps/client/src/translations/en/translation.json index 24e30c6a9..3dcd231bc 100644 --- a/apps/client/src/translations/en/translation.json +++ b/apps/client/src/translations/en/translation.json @@ -1096,6 +1096,12 @@ "vacuuming_database": "Vacuuming database...", "database_vacuumed": "Database has been vacuumed" }, + "experimental_features": { + "title": "Experimental Options", + "disclaimer": "These options are experimental and may cause instability. Use with caution.", + "new_layout_name": "New Layout", + "new_layout_description": "Try out the new layout for a more modern look and improved usability. Subject to heavy change in the upcoming releases." + }, "fonts": { "theme_defined": "Theme defined", "fonts": "Fonts", @@ -1743,7 +1749,9 @@ "printing_pdf": "Exporting to PDF in progress..." }, "note_title": { - "placeholder": "type note's title here..." + "placeholder": "type note's title here...", + "created_on": "Created on {{date}}", + "last_modified": "Last modified on {{date}}" }, "search_result": { "no_notes_found": "No notes have been found for given search parameters.", @@ -2121,5 +2129,11 @@ "tab_history_navigation_buttons": { "go-back": "Go back to previous note", "go-forward": "Go forward to next note" + }, + "breadcrumb_badges": { + "read_only_explicit": "Read-only", + "read_only_auto": "Auto read-only", + "shared_publicly": "Shared publicly", + "shared_locally": "Shared locally" } } diff --git a/apps/client/src/widgets/Breadcrumb.css b/apps/client/src/widgets/Breadcrumb.css index 9739f33db..5f3bc886b 100644 --- a/apps/client/src/widgets/Breadcrumb.css +++ b/apps/client/src/widgets/Breadcrumb.css @@ -1,5 +1,13 @@ .breadcrumb-row { position: relative; + height: 30px; + min-height: 30px; + align-items: center; + padding: 10px; +} + +body.experimental-feature-new-layout .breadcrumb-row { + padding-inline-end: 0; } .component.breadcrumb { diff --git a/apps/client/src/widgets/BreadcrumbBadges.css b/apps/client/src/widgets/BreadcrumbBadges.css new file mode 100644 index 000000000..c7e461113 --- /dev/null +++ b/apps/client/src/widgets/BreadcrumbBadges.css @@ -0,0 +1,23 @@ +.component.breadcrumb-badges { + display: flex; + gap: 5px; + contain: none; + + .breadcrumb-badge { + display: flex; + align-items: center; + padding: 2px 6px; + border-radius: 12px; + font-size: 0.75em; + background-color: var(--badge-background-color); + color: var(--badge-text-color); + + &.clickable { + cursor: pointer; + + &:hover { + background-color: var(--badge-background-hover-color); + } + } + } +} diff --git a/apps/client/src/widgets/BreadcrumbBadges.tsx b/apps/client/src/widgets/BreadcrumbBadges.tsx new file mode 100644 index 000000000..3bb3c06b6 --- /dev/null +++ b/apps/client/src/widgets/BreadcrumbBadges.tsx @@ -0,0 +1,56 @@ +import "./BreadcrumbBadges.css"; + +import { ComponentChildren } from "preact"; +import { useIsNoteReadOnly, useNoteContext } 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 ( +
+ + +
+ ); +} + +function ReadOnlyBadge() { + const { note, noteContext } = useNoteContext(); + const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); + const isExplicitReadOnly = note?.isLabelTruthy("readOnly"); + + return (isReadOnly && + enableEditing()}> + {isExplicitReadOnly ? t("breadcrumb_badges.read_only_explicit") : t("breadcrumb_badges.read_only_auto")} + + ); +} + +function ShareBadge() { + const { note } = useNoteContext(); + const { isSharedExternally, link } = useShareInfo(note); + + return (link && + + {isSharedExternally ? t("breadcrumb_badges.shared_publicly") : t("breadcrumb_badges.shared_locally")} + + ); +} + +function Badge({ icon, children, onClick }: { icon: string, children: ComponentChildren, onClick?: () => void }) { + return ( +
+   + {children} +
+ ); +} diff --git a/apps/client/src/widgets/NoteTitleDetails.tsx b/apps/client/src/widgets/NoteTitleDetails.tsx new file mode 100644 index 000000000..793a3a593 --- /dev/null +++ b/apps/client/src/widgets/NoteTitleDetails.tsx @@ -0,0 +1,23 @@ +import { t } from "../services/i18n"; +import { formatDateTime } from "../utils/formatters"; +import { useNoteContext } from "./react/hooks"; +import { joinElements } from "./react/react_utils"; +import { useNoteMetadata } from "./ribbon/NoteInfoTab"; + +export default function NoteTitleDetails() { + const { note } = useNoteContext(); + const { metadata } = useNoteMetadata(note); + + 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")} )} +
  • + ], " • ")} +
    + ); +} diff --git a/apps/client/src/widgets/containers/root_container.ts b/apps/client/src/widgets/containers/root_container.ts index d2622e6e2..03df4a8ec 100644 --- a/apps/client/src/widgets/containers/root_container.ts +++ b/apps/client/src/widgets/containers/root_container.ts @@ -5,6 +5,7 @@ import FlexContainer from "./flex_container.js"; import options from "../../services/options.js"; import type BasicWidget from "../basic_widget.js"; import utils from "../../services/utils.js"; +import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js"; /** * The root container is the top-most widget/container, from which the entire layout derives. @@ -37,6 +38,7 @@ export default class RootContainer extends FlexContainer { this.#setBackdropEffects(); this.#setThemeCapabilities(); this.#setLocaleAndDirection(options.get("locale")); + this.#setExperimentalFeatures(); return super.render(); } @@ -56,7 +58,7 @@ export default class RootContainer extends FlexContainer { if (loadResults.isOptionReloaded("maxContentWidth") || loadResults.isOptionReloaded("centerContent")) { - + this.#setMaxContentWidth(); } } @@ -99,6 +101,12 @@ export default class RootContainer extends FlexContainer { document.body.classList.toggle("theme-supports-background-effects", useBgfx); } + #setExperimentalFeatures() { + for (const featureId of getEnabledExperimentalFeatureIds()) { + document.body.classList.add(`experimental-feature-${featureId}`); + } + } + #setLocaleAndDirection(locale: string) { const correspondingLocale = LOCALES.find(l => l.id === locale); document.body.lang = locale; diff --git a/apps/client/src/widgets/note_title.css b/apps/client/src/widgets/note_title.css index 477d93209..7d39b6b02 100644 --- a/apps/client/src/widgets/note_title.css +++ b/apps/client/src/widgets/note_title.css @@ -28,3 +28,31 @@ body.mobile .note-title-widget input.note-title { 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 { + margin-top: 2em; + 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; +} + +body.experimental-feature-new-layout.prefers-centered-content .title-row, +body.experimental-feature-new-layout.prefers-centered-content .title-details { + margin-inline: auto; +} diff --git a/apps/client/src/widgets/ribbon/NoteActions.tsx b/apps/client/src/widgets/ribbon/NoteActions.tsx index f1af3b20b..6adaf7f61 100644 --- a/apps/client/src/widgets/ribbon/NoteActions.tsx +++ b/apps/client/src/widgets/ribbon/NoteActions.tsx @@ -1,115 +1,115 @@ import { ConvertToAttachmentResponse } from "@triliumnext/commons"; -import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList"; -import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils"; -import { ParentComponent } from "../react/react_utils"; -import { t } from "../../services/i18n" import { useContext } from "preact/hooks"; -import { useIsNoteReadOnly, useNoteLabel, useNoteProperty } from "../react/hooks"; -import { useTriliumOption } from "../react/hooks"; -import ActionButton from "../react/ActionButton" + import appContext, { CommandNames } from "../../components/app_context"; +import NoteContext from "../../components/note_context"; +import FNote from "../../entities/fnote"; import branches from "../../services/branches"; import dialog from "../../services/dialog"; -import Dropdown from "../react/Dropdown"; -import FNote from "../../entities/fnote" -import NoteContext from "../../components/note_context"; +import { t } from "../../services/i18n"; import server from "../../services/server"; import toast from "../../services/toast"; +import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils"; import ws from "../../services/ws"; +import ActionButton from "../react/ActionButton"; +import Dropdown from "../react/Dropdown"; +import { FormDropdownDivider, FormListHeader, 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"; -interface NoteActionsProps { - note?: FNote; - noteContext?: NoteContext; -} +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); -export default function NoteActions({ note, noteContext }: NoteActionsProps) { - return ( - <> - {note && } - {note && note.type !== "launcher" && } - - ); +export default function NoteActions() { + const { note, noteContext } = useNoteContext(); + return ( +
    + {note && !isNewLayout && } + {note && note.type !== "launcher" && } +
    + ); } function RevisionsButton({ note }: { note: FNote }) { - const isEnabled = !["launcher", "doc"].includes(note?.type ?? ""); + const isEnabled = !["launcher", "doc"].includes(note?.type ?? ""); - return (isEnabled && - - ); + return (isEnabled && + + ); } function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { - const parentComponent = useContext(ParentComponent); - const noteType = useNoteProperty(note, "type") ?? ""; - const [ viewType ] = useNoteLabel(note, "viewType"); - const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); - const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType); - const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); - const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? "")); - const isElectron = getIsElectron(); - const isMac = getIsMac(); - const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType); - const isSearchOrBook = ["search", "book"].includes(noteType); - const [ syncServerHost ] = useTriliumOption("syncServerHost"); - const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext); + const parentComponent = useContext(ParentComponent); + const noteType = useNoteProperty(note, "type") ?? ""; + const [viewType] = useNoteLabel(note, "viewType"); + const canBeConvertedToAttachment = note?.isEligibleForConversionToAttachment(); + const isSearchable = ["text", "code", "book", "mindMap", "doc"].includes(noteType); + const isInOptionsOrHelp = note?.noteId.startsWith("_options") || note?.noteId.startsWith("_help"); + const isPrintable = ["text", "code"].includes(noteType) || (noteType === "book" && ["presentation", "list", "table"].includes(viewType ?? "")); + const isElectron = getIsElectron(); + const isMac = getIsMac(); + const hasSource = ["text", "code", "relationMap", "mermaid", "canvas", "mindMap", "aiChat"].includes(noteType); + const isSearchOrBook = ["search", "book"].includes(noteType); + const [syncServerHost] = useTriliumOption("syncServerHost"); + const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext); - return ( - + return ( + - {isReadOnly && <> - enableEditing()} /> - - } + {isReadOnly && <> + enableEditing()} /> + + } - {canBeConvertedToAttachment && } - {note.type === "render" && } - - - {isElectron && } - + {canBeConvertedToAttachment && } + {note.type === "render" && } + + + {isElectron && } + - parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> - noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { - notePath: noteContext.notePath, - defaultType: "single" - })} /> - + parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} /> + noteContext?.notePath && parentComponent?.triggerCommand("showExportDialog", { + notePath: noteContext.notePath, + defaultType: "single" + })} /> + - - - - {(syncServerHost && isElectron) && - - } - + + + + {(syncServerHost && isElectron) && + + } + - - branches.deleteNotes([note.getParentBranches()[0].branchId])} - /> - + + + branches.deleteNotes([note.getParentBranches()[0].branchId])} + /> + - - {glob.isDev && } - - ); + + {glob.isDev && } + + ); } function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) { @@ -129,46 +129,46 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?: throw new Error("Editor crashed."); }); }); - }}>Crash editor)} + }}>Crash editor)} - ) + ); } function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) { - return {text} + return {text}; } function ConvertToAttachment({ note }: { note: FNote }) { - return ( - { - if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) { - return; - } + return ( + { + if (!note || !(await dialog.confirm(t("note_actions.convert_into_attachment_prompt", { title: note.title })))) { + return; + } - const { attachment: newAttachment } = await server.post(`notes/${note.noteId}/convert-to-attachment`); + const { attachment: newAttachment } = await server.post(`notes/${note.noteId}/convert-to-attachment`); - if (!newAttachment) { - toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title })); - return; - } + if (!newAttachment) { + toast.showMessage(t("note_actions.convert_into_attachment_failed", { title: note.title })); + return; + } - toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); - await ws.waitForMaxKnownEntityChangeId(); - await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { - viewScope: { - viewMode: "attachments", - attachmentId: newAttachment.attachmentId - } - }); - }} - >{t("note_actions.convert_into_attachment")} - ) + toast.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title })); + await ws.waitForMaxKnownEntityChangeId(); + await appContext.tabManager.getActiveContext()?.setNote(newAttachment.ownerId, { + viewScope: { + viewMode: "attachments", + attachmentId: newAttachment.attachmentId + } + }); + }} + >{t("note_actions.convert_into_attachment")} + ); } diff --git a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx index 5901db3a2..888412867 100644 --- a/apps/client/src/widgets/ribbon/NoteInfoTab.tsx +++ b/apps/client/src/widgets/ribbon/NoteInfoTab.tsx @@ -8,30 +8,13 @@ import { formatDateTime } from "../../utils/formatters"; import { formatSize } from "../../services/utils"; import LoadingSpinner from "../react/LoadingSpinner"; import { useTriliumEvent } from "../react/hooks"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; +import FNote from "../../entities/fnote"; + +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); export default function NoteInfoTab({ note }: TabContext) { - const [ metadata, setMetadata ] = useState(); - const [ isLoading, setIsLoading ] = useState(false); - const [ noteSizeResponse, setNoteSizeResponse ] = useState(); - const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState(); - - function refresh() { - if (note) { - server.get(`notes/${note?.noteId}/metadata`).then(setMetadata); - } - - setNoteSizeResponse(undefined); - setSubtreeSizeResponse(undefined); - setIsLoading(false); - } - - useEffect(refresh, [ note?.noteId ]); - useTriliumEvent("entitiesReloaded", ({ loadResults }) => { - const noteId = note?.noteId; - if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) { - refresh(); - } - }); + const { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo } = useNoteMetadata(note); return (
    @@ -41,14 +24,14 @@ export default function NoteInfoTab({ note }: TabContext) { {t("note_info_widget.note_id")}: {note.noteId}
    -
    + {!isNewLayout &&
    {t("note_info_widget.created")}: {formatDateTime(metadata?.dateCreated)} -
    -
    +
    } + {!isNewLayout &&
    {t("note_info_widget.modified")}: {formatDateTime(metadata?.dateModified)} -
    +
    }
    {t("note_info_widget.type")}: @@ -64,16 +47,7 @@ export default function NoteInfoTab({ note }: TabContext) { className="calculate-button" icon="bx bx-calculator" text={t("note_info_widget.calculate")} - onClick={() => { - setIsLoading(true); - setTimeout(async () => { - await Promise.allSettled([ - server.get(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse), - server.get(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse) - ]); - setIsLoading(false); - }, 0); - }} + onClick={requestSizeInfo} /> )} @@ -90,5 +64,45 @@ export default function NoteInfoTab({ note }: TabContext) { )}
    - ) + ); +} + +export function useNoteMetadata(note: FNote | null | undefined) { + const [ isLoading, setIsLoading ] = useState(false); + const [ noteSizeResponse, setNoteSizeResponse ] = useState(); + const [ subtreeSizeResponse, setSubtreeSizeResponse ] = useState(); + const [ metadata, setMetadata ] = useState(); + + function refresh() { + if (note) { + server.get(`notes/${note?.noteId}/metadata`).then(setMetadata); + } + + setNoteSizeResponse(undefined); + setSubtreeSizeResponse(undefined); + setIsLoading(false); + } + + function requestSizeInfo() { + if (!note) return; + + setIsLoading(true); + setTimeout(async () => { + await Promise.allSettled([ + server.get(`stats/note-size/${note.noteId}`).then(setNoteSizeResponse), + server.get(`stats/subtree-size/${note.noteId}`).then(setSubtreeSizeResponse) + ]); + setIsLoading(false); + }, 0); + } + + useEffect(refresh, [ note ]); + useTriliumEvent("entitiesReloaded", ({ loadResults }) => { + const noteId = note?.noteId; + if (noteId && (loadResults.isNoteReloaded(noteId) || loadResults.isNoteContentReloaded(noteId))) { + refresh(); + } + }); + + return { isLoading, metadata, noteSizeResponse, subtreeSizeResponse, requestSizeInfo }; } diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index ee3fd3fcf..3afd39b56 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -4,11 +4,11 @@ import "./style.css"; import { Indexed, numberObjectsInPlace } from "../../services/utils"; import { EventNames } from "../../components/app_context"; -import NoteActions from "./NoteActions"; import { KeyboardActionNames } from "@triliumnext/commons"; import { RIBBON_TAB_DEFINITIONS } from "./RibbonDefinition"; import { TabConfiguration, TitleContext } from "./ribbon-interface"; import clsx from "clsx"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; const TAB_CONFIGURATION = numberObjectsInPlace(RIBBON_TAB_DEFINITIONS); @@ -16,7 +16,9 @@ interface ComputedTab extends Indexed { shouldShow: boolean; } -export default function Ribbon() { +const isNewLayout = isExperimentalFeatureEnabled("new-layout"); + +export default function Ribbon({ children }: { children?: preact.ComponentChildren }) { const { note, ntxId, hoistedNoteId, notePath, noteContext, componentId, isReadOnlyTemporarilyDisabled } = useNoteContext(); const noteType = useNoteProperty(note, "type"); const [ activeTabIndex, setActiveTabIndex ] = useState(); @@ -29,7 +31,8 @@ export default function Ribbon() { async function refresh() { const computedTabs: ComputedTab[] = []; for (const tab of TAB_CONFIGURATION) { - const shouldShow = await shouldShowTab(tab.show, titleContext); + const shouldAvoid = (isNewLayout && tab.avoidInNewLayout); + const shouldShow = !shouldAvoid && await shouldShowTab(tab.show, titleContext); computedTabs.push({ ...tab, shouldShow: !!shouldShow @@ -99,9 +102,7 @@ export default function Ribbon() { /> ))} -
    - { note && } -
    + {children}
    diff --git a/apps/client/src/widgets/ribbon/RibbonDefinition.ts b/apps/client/src/widgets/ribbon/RibbonDefinition.ts index 0eb63002a..280f7cdda 100644 --- a/apps/client/src/widgets/ribbon/RibbonDefinition.ts +++ b/apps/client/src/widgets/ribbon/RibbonDefinition.ts @@ -16,6 +16,7 @@ import FormattingToolbar from "./FormattingToolbar"; import options from "../../services/options"; import { t } from "../../services/i18n"; import { TabConfiguration } from "./ribbon-interface"; +import { isExperimentalFeatureEnabled } from "../../services/experimental_features"; export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ { @@ -27,7 +28,8 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [ toggleCommand: "toggleRibbonTabClassicEditor", content: FormattingToolbar, activate: ({ note }) => !options.is("editedNotesOpenInRibbon") || !note?.hasOwnedLabel("dateNote"), - stayInDom: true + stayInDom: !isExperimentalFeatureEnabled("new-layout"), + avoidInNewLayout: true }, { title: ({ note }) => note?.isTriliumSqlite() ? t("script_executor.query") : t("script_executor.script"), diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts index 7ab982dd2..a83bbc55f 100644 --- a/apps/client/src/widgets/ribbon/ribbon-interface.ts +++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts @@ -30,4 +30,5 @@ export interface TabConfiguration { * By default the tab content will not be rendered unless the tab is active (i.e. selected by the user). Setting to `true` will ensure that the tab is rendered even when inactive, for cases where the tab needs to be accessible at all times (e.g. for the detached editor toolbar) or if event handling is needed. */ stayInDom?: boolean; + avoidInNewLayout?: boolean; } diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 8c2a5fbb5..b1813d65f 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -415,3 +415,24 @@ body[dir=rtl] .attribute-list-editor { pointer-events: none; /* makes it unclickable */ } /* #endregion */ + +/* #region Experimental layout */ +body.experimental-feature-new-layout { + .ribbon-container { + display: flex; + flex-direction: column-reverse; + border-top: 1px solid var(--main-border-color); + + .ribbon-tab-spacer, + .ribbon-tab-title, + .ribbon-body { + border-bottom: 0 !important; + } + } + + .ribbon-button-container { + border-bottom: 0 !important; + margin: 0; + } +} +/* #endregion */ diff --git a/apps/client/src/widgets/shared_info.tsx b/apps/client/src/widgets/shared_info.tsx index bd0b72bc2..954ceb5f0 100644 --- a/apps/client/src/widgets/shared_info.tsx +++ b/apps/client/src/widgets/shared_info.tsx @@ -10,8 +10,23 @@ import RawHtml from "./react/RawHtml"; export default function SharedInfo() { const { note } = useNoteContext(); - const [ syncServerHost ] = useTriliumOption("syncServerHost"); + const { isSharedExternally, link } = useShareInfo(note); + + return ( + + {link && ( + + )} + + + ); +} + +export function useShareInfo(note: FNote | null | undefined) { const [ link, setLink ] = useState(); + const [ syncServerHost ] = useTriliumOption("syncServerHost"); function refresh() { if (!note) return; @@ -48,16 +63,7 @@ export default function SharedInfo() { } }); - return ( - - {link && ( - - )} - - - ) + return { link, isSharedExternally: !!syncServerHost }; } function getShareId(note: FNote) { @@ -66,4 +72,4 @@ function getShareId(note: FNote) { } return note.getOwnedLabelValue("shareAlias") || note.noteId; -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/type_widgets/options/advanced.tsx b/apps/client/src/widgets/type_widgets/options/advanced.tsx index b16dca89d..958180063 100644 --- a/apps/client/src/widgets/type_widgets/options/advanced.tsx +++ b/apps/client/src/widgets/type_widgets/options/advanced.tsx @@ -7,6 +7,9 @@ import FormText from "../../react/FormText"; import OptionsSection from "./components/OptionsSection" import Column from "../../react/Column"; import { useEffect, useState } from "preact/hooks"; +import CheckboxList from "./components/CheckboxList"; +import { experimentalFeatures } from "../../../services/experimental_features"; +import { useTriliumOptionJson } from "../../react/hooks"; export default function AdvancedSettings() { return <> @@ -14,6 +17,7 @@ export default function AdvancedSettings() { + ; } @@ -44,14 +48,14 @@ function DatabaseIntegrityOptions() { return ( {t("database_integrity_check.description")} - +