New layout refinement (#8088)
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run

This commit is contained in:
Elian Doran 2025-12-17 18:51:38 +02:00 committed by GitHub
commit 231ec39025
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 546 additions and 156 deletions

View File

@ -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}`);
}
}
}

View File

@ -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;

View File

@ -20,11 +20,19 @@ export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
let enabledFeatures: Set<ExperimentalFeatureId> | 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) {

View File

@ -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;
}

View File

@ -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": "Weve 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": "%"

View File

@ -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<SVGSVGElement>(null);
useStaticTooltip(logoRef);
return (
<Dropdown
className="global-menu"
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
text={<>
{isVerticalLayout && <VerticalLayoutIcon />}
{isVerticalLayout && <VerticalLayoutIcon logoRef={logoRef} />}
{isUpdateAvailable && <div class="global-menu-button-update-available">
<span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")} />
</div>}
@ -159,10 +161,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<Keybo
/>;
}
function VerticalLayoutIcon() {
const logoRef = useRef<SVGSVGElement>(null);
useStaticTooltip(logoRef);
export function VerticalLayoutIcon({ logoRef }: { logoRef?: RefObject<SVGSVGElement> }) {
return (
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
<g>

View File

@ -1,8 +1,9 @@
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(), []);
@ -40,7 +41,7 @@ export default function CallToActionDialog() {
)}
</>}
>
<p>{activeItem.message}</p>
<p className="pre-wrap-text">{activeItem.message}</p>
</Modal>
)
);
}

View File

@ -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");
}
}
]
}
];

View File

@ -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;

View File

@ -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 <Badge
icon="bx bx-lock-open-alt"
text={t("breadcrumb_badges.read_only_temporarily_disabled")}

View File

@ -1,7 +1,7 @@
import "./NoteTitleActions.css";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import { useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
@ -10,7 +10,7 @@ import CollectionProperties from "../note_bars/CollectionProperties";
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import { useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks";
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
export default function NoteTitleActions() {
@ -57,6 +57,9 @@ function PromotedAttributes({ note, componentId, noteContext }: {
});
}, [ note, noteContext ]);
// Keyboard shortcut.
useTriliumEvent("toggleRibbonTabPromotedAttributes", () => setExpanded(!expanded));
if (!cells?.length) return false;
return (note && (
<ExternallyControlledCollapsible

View File

@ -1,6 +1,6 @@
import "./StatusBar.css";
import { Locale } from "@triliumnext/commons";
import { KeyboardActionNames, Locale } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { type ComponentChildren } from "preact";
@ -9,7 +9,7 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import FNote, { NOTE_TYPE_ICONS } from "../../entities/fnote";
import attributes from "../../services/attributes";
import { t } from "../../services/i18n";
import { ViewScope } from "../../services/link";
@ -216,14 +216,19 @@ interface NoteInfoContext extends StatusBarContext {
setSimilarNotesShown: (value: boolean) => void;
}
export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) {
export function NoteInfoBadge({ note, similarNotesShown, setSimilarNotesShown }: NoteInfoContext) {
const dropdownRef = useRef<BootstrapDropdown>(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 &&
<StatusBarDropdown
icon="bx bx-info-circle"
title={t("status_bar.note_info_title")}
@ -235,7 +240,7 @@ export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) {
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
<NoteInfoValue text={t("note_info_widget.type")} value={<><Icon icon={`bx ${currentNoteTypeData.icon}`}/>{" "}{currentNoteTypeData?.title}</>} />
<NoteInfoValue text={t("note_info_widget.type")} value={<><Icon icon={`bx ${noteTypeMapping.icon ?? NOTE_TYPE_ICONS[noteType]}`}/>{" "}{noteTypeMapping?.title}</>} />
{note.mime && <NoteInfoValue text={t("note_info_widget.mime")} value={note.mime} />}
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
@ -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<BootstrapDropdown>(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 &&
<StatusBarDropdown
title={t("status_bar.note_paths_title")}
dropdownRef={dropdownRef}
dropdownContainerClassName="dropdown-note-paths"
icon="bx bx-directions"
text={t("status_bar.note_paths", { count })}

View File

@ -1,7 +1,7 @@
import "./CollectionProperties.css";
import { t } from "i18next";
import { useContext } from "preact/hooks";
import { useContext, useRef } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import FNote from "../../entities/fnote";
@ -12,7 +12,7 @@ import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
import FormTextBox from "../react/FormTextBox";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault } from "../react/hooks";
import { useNoteLabel, useNoteLabelBoolean, useNoteLabelWithDefault, useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
@ -42,8 +42,15 @@ export default function CollectionProperties({ note }: { note: FNote }) {
}
function ViewTypeSwitcher({ viewType, setViewType }: { viewType: ViewTypeOptions, setViewType: (newValue: ViewTypeOptions) => void }) {
// Keyboard shortcut
const dropdownContainerRef = useRef<HTMLDivElement>(null);
useTriliumEvent("toggleRibbonTabBookProperties", () => {
dropdownContainerRef.current?.querySelector("button")?.focus();
});
return (
<Dropdown
dropdownContainerRef={dropdownContainerRef}
text={<>
<Icon icon={ICON_MAPPINGS[viewType]} />&nbsp;
{VIEW_TYPE_MAPPINGS[viewType]}

View File

@ -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<HTMLLIElement>;
}
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
@ -104,8 +105,8 @@ const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
};
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
const itemRef = useRef<HTMLLIElement>(null);
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, itemRef: externalItemRef, ...contentProps }: FormListItemOpts) {
const itemRef = useSyncedRef<HTMLLIElement>(externalItemRef, null);
if (checked) {
icon = "bx bx-check";

View File

@ -933,11 +933,13 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(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) {

View File

@ -49,6 +49,21 @@ export function FixedFormattingToolbar() {
const renderState = useRenderState(noteContext, note);
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
// Keyboard shortcut.
const lastFocusedElement = useRef<Element>(null);
useTriliumEvent("toggleRibbonTabClassicEditor", () => {
if (!toolbarToRender) return;
if (!toolbarToRender.contains(document.activeElement)) {
// Focus to the fixed formatting toolbar.
lastFocusedElement.current = document.activeElement;
toolbarToRender.querySelector<HTMLButtonElement>(".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;

View File

@ -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<BootstrapDropdown>(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<ItemToFocus>(null);
// Keyboard shortcuts.
useTriliumEvent("toggleRibbonTabBasicProperties", () => {
if (!isNewLayout) return;
itemToFocusRef.current = "basic-properties";
dropdownRef.current?.toggle();
});
return (
<Dropdown
dropdownRef={dropdownRef}
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
className="note-actions"
hideToggleArrow
noSelectButtonStyle
iconAction>
iconAction
onHidden={() => itemToFocusRef.current = null }
>
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
@ -99,7 +115,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
<FormDropdownDivider />
{isNewLayout && isNormalViewMode && !isHelpPage && <>
<NoteBasicProperties note={note} />
<NoteBasicProperties note={note} focus={itemToFocusRef} />
<FormDropdownDivider />
</>}
@ -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<ItemToFocus>;
}) {
const itemToFocusRef = useRef<HTMLLIElement>(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 <>
<FormListToggleableItem
icon="bx bx-share-alt"
@ -161,6 +187,7 @@ function NoteBasicProperties({ note }: { note: FNote }) {
currentValue={isShared} onChange={switchShareState}
helpPage="R9pX4DGra2Vt"
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
itemRef={itemToFocusRef}
/>
<FormListToggleableItem
icon="bx bx-lock-alt"

View File

@ -1,5 +1,5 @@
import { NoteType } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
import Component from "../../components/component";
import NoteContext from "../../components/note_context";
@ -12,7 +12,7 @@ import { ViewTypeOptions } from "../collections/interface";
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
import ActionButton from "../react/ActionButton";
import { FormFileUploadActionButton } from "../react/FormFileUpload";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
@ -38,6 +38,7 @@ interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
*/
export default function NoteActionsCustom(props: NoteActionsCustomProps) {
const { note } = props;
const containerRef = useRef<HTMLDivElement>(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 &&
<div className="note-actions-custom">
<div
ref={containerRef}
className="note-actions-custom"
>
<AddChildButton {...innerProps} />
<RunActiveNoteButton {...innerProps } />
<OpenTriliumApiDocsButton {...innerProps} />

View File

@ -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<CommandData>;
@ -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<void>;
refresh: () => void;
renderOwnedAttributes: (ownedAttributes: FAttribute[]) => Promise<void>;
save(): Promise<void>;
refresh(): void;
focus(): void;
renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise<void>;
}
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 = ("<p>" + (await attribute_renderer.renderAttributes(ownedAttributes, true)).html() + "</p>");
let htmlAttrs = (`<p>${(await attribute_renderer.renderAttributes(ownedAttributes, true)).html()}</p>`);
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) {

View File

@ -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;
}
}
}
}
}

View File

@ -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 (
<div>
{!isMobile() && <LayoutSwitcher />}
{!isMobile() && <LayoutOrientation />}
<ApplicationTheme />
{overrideThemeFonts === "true" && <Fonts />}
@ -102,7 +109,99 @@ export default function AppearanceSettings() {
}
]} />
</div>
)
);
}
function LayoutSwitcher() {
const [ newLayout, setNewLayout ] = useTriliumOptionBool("newLayout");
return (
<OptionsSection title={t("settings_appearance.ui")}>
<RadioWithIllustration
currentValue={newLayout ? "new-layout" : "old-layout"}
onChange={async newValue => {
await setNewLayout(newValue === "new-layout");
reloadFrontendApp();
}}
values={[
{ key: "old-layout", text: t("settings_appearance.ui_old_layout"), illustration: <LayoutIllustration /> },
{ key: "new-layout", text: t("settings_appearance.ui_new_layout"), illustration: <LayoutIllustration isNewLayout /> }
]}
/>
</OptionsSection>
);
}
function LayoutIllustration({ isNewLayout }: { isNewLayout?: boolean }) {
return (
<div className="old-layout-illustration">
<div className="launcher-pane">
<VerticalLayoutIcon />
<Icon icon="bx bx-send" />
<Icon icon="bx bx-file-blank" />
<Icon icon="bx bx-search" />
</div>
<div className="tree">
<ul>
<li>Options</li>
<ul>
<li>Appearance</li>
<li>Shortcuts</li>
<li>Text Notes</li>
<li>Code Notes</li>
<li>Images</li>
</ul>
</ul>
</div>
<div className="main">
<div className="tab-bar" />
<div className="content">
<div className="title-bar">
<Icon icon="bx bx-note" />
<span className="title">Title</span>
<Icon icon="bx bx-dock-right" />
</div>
{!isNewLayout && <div className="ribbon">
<div className="ribbon-header">
<Icon icon="bx bx-slider" />
<Icon icon="bx bx-list-check" />
<Icon icon="bx bx-list-plus" />
<Icon icon="bx bx-collection" />
</div>
<div className="ribbon-body" />
</div>}
{isNewLayout && <div className="note-title-actions">
<Icon icon="bx bx-chevron-down" />{" "}Promoted attributes
</div>}
<div className="content-inner">
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.
</div>
{isNewLayout && <div className="status-bar">
<div className="status-bar-breadcrumb">
<Icon icon="bx bx-home" />
<Icon icon="bx bx-chevron-right" />
Note
<Icon icon="bx bx-chevron-right" />
Note
</div>
<div className="status-bar-actions">
<Icon icon="bx bx-list-check" />
<Icon icon="bx bx-info-circle" />
</div>
</div>}
</div>
</div>
</div>
);
}
function LayoutOrientation() {
@ -141,7 +240,7 @@ function ApplicationTheme() {
setThemes([
...BUILTIN_THEMES,
...userThemes
])
]);
});
}, []);
@ -162,7 +261,7 @@ function ApplicationTheme() {
</FormGroup>
</div>
</OptionsSection>
)
);
}
function Fonts() {
@ -245,7 +344,7 @@ function ElectronIntegration() {
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
</OptionsSection>
)
);
}
function Performance() {
@ -271,7 +370,7 @@ function Performance() {
{isElectron() && <SmoothScrollEnabledOption />}
</OptionsSection>
</OptionsSection>;
}
function SmoothScrollEnabledOption() {
@ -280,7 +379,7 @@ function SmoothScrollEnabledOption() {
return <FormCheckbox
label={`${t("ui-performance.enable-smooth-scroll")} ${t("ui-performance.app-restart-required")}`}
currentValue={smoothScrollEnabled} onChange={setSmoothScrollEnabled}
/>
/>;
}
function MaxContentWidth() {
@ -302,10 +401,10 @@ function MaxContentWidth() {
</Column>
<FormCheckbox label={t("max_content_width.centerContent")}
currentValue={centerContent}
onChange={setCenterContent} />
currentValue={centerContent}
onChange={setCenterContent} />
</OptionsSection>
)
);
}
function RibbonOptions() {
@ -318,5 +417,5 @@ function RibbonOptions() {
currentValue={editedNotesOpenInRibbon} onChange={setEditedNotesOpenInRibbon}
/>
</OptionsSection>
)
);
}

View File

@ -8,6 +8,7 @@
.option-row > label {
width: 40%;
margin-bottom: 0 !important;
flex-shrink: 0;
}
.option-row > select,

View File

@ -0,0 +1,28 @@
.options-section .radio-with-illustration {
list-style-type: none;
margin-bottom: 0;
padding: 0;
display: flex;
gap: 1.5em;
justify-content: center;
figure {
figcaption {
margin-top: 0.25em;
text-align: center;
}
margin-bottom: 0;
&> .illustration {
border-radius: 6px;
padding: 3px;
cursor: pointer;
}
}
&> .selected figure > .illustration {
outline: 3px solid var(--input-focus-outline-color);
}
}

View File

@ -0,0 +1,38 @@
import "./RadioWithIllustration.css";
import clsx from "clsx";
import { ComponentChild } from "preact";
interface RadioWithIllustrationProps {
values: {
key: string;
text: string;
illustration: ComponentChild;
}[];
currentValue: string;
onChange(newValue: string): void;
}
export default function RadioWithIllustration({ currentValue, onChange, values }: RadioWithIllustrationProps) {
return (
<ul className="radio-with-illustration">
{values.map(value => (
<li
key={value.key}
className={clsx(value.key === currentValue && "selected")}
>
<figure>
<div
className="illustration"
role="button"
onClick={() => onChange(value.key)}
>
{value.illustration}
</div>
<figcaption>{value.text}</figcaption>
</figure>
</li>
))}
</ul>
);
}

Binary file not shown.

View File

@ -1,13 +1,14 @@
"use strict";
import optionService from "../../services/options.js";
import log from "../../services/log.js";
import searchService from "../../services/search/services/search.js";
import ValidationError from "../../errors/validation_error.js";
import type { Request } from "express";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import type { OptionNames } from "@triliumnext/commons";
import type { Request } from "express";
import ValidationError from "../../errors/validation_error.js";
import config from "../../services/config.js";
import { changeLanguage, getLocales } from "../../services/i18n.js";
import log from "../../services/log.js";
import optionService from "../../services/options.js";
import searchService from "../../services/search/services/search.js";
interface UserTheme {
val: string; // value of the theme, used in the URL
@ -100,6 +101,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
"splitEditorOrientation",
"seenCallToActions",
"experimentalFeatures",
"newLayout",
// AI/LLM integration options
"aiEnabled",

View File

@ -1,10 +1,11 @@
import optionService from "./options.js";
import { type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames, SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
import appInfo from "./app_info.js";
import { randomSecureToken, isWindows } from "./utils.js";
import log from "./log.js";
import dateUtils from "./date_utils.js";
import keyboardActions from "./keyboard_actions.js";
import { SANITIZER_DEFAULT_ALLOWED_TAGS, type KeyboardShortcutWithRequiredActionName, type OptionMap, type OptionNames } from "@triliumnext/commons";
import log from "./log.js";
import optionService from "./options.js";
import { isWindows,randomSecureToken } from "./utils.js";
function initDocumentOptions() {
optionService.createOption("documentId", randomSecureToken(16), false);
@ -156,6 +157,7 @@ const defaultOptions: DefaultOption[] = [
{ name: "shadowsEnabled", value: "true", isSynced: false },
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
{ name: "smoothScrollEnabled", value: "true", isSynced: false },
{ name: "newLayout", value: "true", isSynced: true },
// Internationalization
{ name: "locale", value: "en", isSynced: true },
@ -171,9 +173,9 @@ const defaultOptions: DefaultOption[] = [
value: (optionsMap) => {
if (optionsMap.theme === "light") {
return "default:stackoverflow-light";
} else {
return "default:stackoverflow-dark";
}
return "default:stackoverflow-dark";
},
isSynced: false
},

View File

@ -131,6 +131,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
/** Whether keyboard auto-completion for editing commands is triggered when typing `/`. */
textNoteSlashCommandsEnabled: boolean;
backgroundEffects: boolean;
newLayout: boolean;
// Share settings
redirectBareDomain: boolean;