mirror of
https://github.com/zadam/trilium.git
synced 2025-12-20 22:34:23 +01:00
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
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:
commit
231ec39025
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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": "%"
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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")}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 })}
|
||||
|
||||
@ -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]} />
|
||||
{VIEW_TYPE_MAPPINGS[viewType]}
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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) {
|
||||
|
||||
114
apps/client/src/widgets/type_widgets/options/appearance.css
Normal file
114
apps/client/src/widgets/type_widgets/options/appearance.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
.option-row > label {
|
||||
width: 40%;
|
||||
margin-bottom: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.option-row > select,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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.
@ -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",
|
||||
|
||||
@ -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
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user