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 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 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 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 {
|
export default class RootCommandExecutor extends Component {
|
||||||
editReadOnlyNoteCommand() {
|
editReadOnlyNoteCommand() {
|
||||||
@ -193,10 +193,13 @@ export default class RootCommandExecutor extends Component {
|
|||||||
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
appContext.triggerEvent("zenModeChanged", { isEnabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
async toggleRibbonTabNoteMapCommand() {
|
async toggleRibbonTabNoteMapCommand(data: CommandListenerData<"toggleRibbonTabNoteMap">) {
|
||||||
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
|
const { isExperimentalFeatureEnabled } = await import("../services/experimental_features.js");
|
||||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||||
if (!isNewLayout) return;
|
if (!isNewLayout) {
|
||||||
|
this.triggerEvent("toggleRibbonTabNoteMap", data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const activeContext = appContext.tabManager.getActiveContext();
|
const activeContext = appContext.tabManager.getActiveContext();
|
||||||
if (!activeContext?.notePath) return;
|
if (!activeContext?.notePath) return;
|
||||||
@ -272,7 +275,7 @@ export default class RootCommandExecutor extends Component {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error("Error creating AI Chat note:", 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 cssClassManager from "../services/css_class_manager.js";
|
||||||
import type { Froca } from "../services/froca-interface.js";
|
import type { Froca } from "../services/froca-interface.js";
|
||||||
import type FAttachment from "./fattachment.js";
|
import noteAttributeCache from "../services/note_attribute_cache.js";
|
||||||
import type { default as FAttribute, AttributeType } from "./fattribute.js";
|
import protectedSessionHolder from "../services/protected_session_holder.js";
|
||||||
import utils from "../services/utils.js";
|
|
||||||
import search from "../services/search.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 LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
|
|
||||||
const NOTE_TYPE_ICONS = {
|
export const NOTE_TYPE_ICONS = {
|
||||||
file: "bx bx-file",
|
file: "bx bx-file",
|
||||||
image: "bx bx-image",
|
image: "bx bx-image",
|
||||||
code: "bx bx-code",
|
code: "bx bx-code",
|
||||||
@ -268,13 +268,12 @@ export default class FNote {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
} else {
|
|
||||||
return this.children;
|
|
||||||
}
|
}
|
||||||
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSubtreeNoteIds(includeArchived = false) {
|
async getSubtreeNoteIds(includeArchived = false) {
|
||||||
let noteIds: (string | string[])[] = [];
|
const noteIds: (string | string[])[] = [];
|
||||||
for (const child of await this.getChildNotes()) {
|
for (const child of await this.getChildNotes()) {
|
||||||
if (child.isArchived && !includeArchived) continue;
|
if (child.isArchived && !includeArchived) continue;
|
||||||
|
|
||||||
@ -471,9 +470,8 @@ export default class FNote {
|
|||||||
return a.isHidden ? 1 : -1;
|
return a.isHidden ? 1 : -1;
|
||||||
} else if (a.isSearch !== b.isSearch) {
|
} else if (a.isSearch !== b.isSearch) {
|
||||||
return a.isSearch ? 1 : -1;
|
return a.isSearch ? 1 : -1;
|
||||||
} else {
|
|
||||||
return a.notePath.length - b.notePath.length;
|
|
||||||
}
|
}
|
||||||
|
return a.notePath.length - b.notePath.length;
|
||||||
});
|
});
|
||||||
|
|
||||||
return notePaths;
|
return notePaths;
|
||||||
@ -597,14 +595,12 @@ export default class FNote {
|
|||||||
} else if (this.type === "text") {
|
} else if (this.type === "text") {
|
||||||
if (this.isFolder()) {
|
if (this.isFolder()) {
|
||||||
return "bx bx-folder";
|
return "bx bx-folder";
|
||||||
} else {
|
|
||||||
return "bx bx-note";
|
|
||||||
}
|
}
|
||||||
|
return "bx bx-note";
|
||||||
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
} else if (this.type === "code" && this.mime.startsWith("text/x-sql")) {
|
||||||
return "bx bx-data";
|
return "bx bx-data";
|
||||||
} else {
|
|
||||||
return NOTE_TYPE_ICONS[this.type];
|
|
||||||
}
|
}
|
||||||
|
return NOTE_TYPE_ICONS[this.type];
|
||||||
}
|
}
|
||||||
|
|
||||||
getColorClass() {
|
getColorClass() {
|
||||||
@ -617,7 +613,7 @@ export default class FNote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilteredChildBranches() {
|
getFilteredChildBranches() {
|
||||||
let childBranches = this.getChildBranches();
|
const childBranches = this.getChildBranches();
|
||||||
|
|
||||||
if (!childBranches) {
|
if (!childBranches) {
|
||||||
console.error(`No children for '${this.noteId}'. This shouldn't happen.`);
|
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));
|
return this.getLabelValue(nameWithPrefix.substring(1));
|
||||||
} else if (nameWithPrefix.startsWith("~")) {
|
} else if (nameWithPrefix.startsWith("~")) {
|
||||||
return this.getRelationValue(nameWithPrefix.substring(1));
|
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) => {
|
promotedAttrs.sort((a, b) => {
|
||||||
if (a.noteId === b.noteId) {
|
if (a.noteId === b.noteId) {
|
||||||
return a.position < b.position ? -1 : 1;
|
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;
|
return promotedAttrs;
|
||||||
|
|||||||
@ -20,11 +20,19 @@ export type ExperimentalFeatureId = typeof experimentalFeatures[number]["id"];
|
|||||||
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
|
||||||
|
|
||||||
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
|
||||||
|
if (featureId === "new-layout") {
|
||||||
|
return options.is("newLayout");
|
||||||
|
}
|
||||||
|
|
||||||
return getEnabledFeatures().has(featureId);
|
return getEnabledFeatures().has(featureId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEnabledExperimentalFeatureIds() {
|
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) {
|
export async function toggleExperimentalFeature(featureId: ExperimentalFeatureId, enable: boolean) {
|
||||||
|
|||||||
@ -717,12 +717,17 @@ table.promoted-attributes-in-tooltip th {
|
|||||||
.tooltip {
|
.tooltip {
|
||||||
font-size: var(--main-font-size) !important;
|
font-size: var(--main-font-size) !important;
|
||||||
z-index: calc(var(--ck-z-panel) - 1) !important;
|
z-index: calc(var(--ck-z-panel) - 1) !important;
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip.tooltip-top {
|
.tooltip.tooltip-top {
|
||||||
z-index: 32767 !important;
|
z-index: 32767 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pre-wrap-text {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.bs-tooltip-bottom .tooltip-arrow::before {
|
.bs-tooltip-bottom .tooltip-arrow::before {
|
||||||
border-bottom-color: var(--main-border-color) !important;
|
border-bottom-color: var(--main-border-color) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2109,6 +2109,8 @@
|
|||||||
"background_effects_title": "Background effects are now stable",
|
"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_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",
|
"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"
|
"dismiss": "Dismiss"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@ -2116,7 +2118,10 @@
|
|||||||
},
|
},
|
||||||
"settings_appearance": {
|
"settings_appearance": {
|
||||||
"related_code_blocks": "Color scheme for code blocks in text notes",
|
"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": {
|
"units": {
|
||||||
"percentage": "%"
|
"percentage": "%"
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "./global_menu.css";
|
import "./global_menu.css";
|
||||||
|
|
||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren, RefObject } from "preact";
|
||||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { CommandNames } from "../../components/app_context";
|
import { CommandNames } from "../../components/app_context";
|
||||||
@ -30,13 +30,15 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
|
|||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||||
const isMobileLocal = isMobile();
|
const isMobileLocal = isMobile();
|
||||||
|
const logoRef = useRef<SVGSVGElement>(null);
|
||||||
|
useStaticTooltip(logoRef);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="global-menu"
|
className="global-menu"
|
||||||
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
|
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
|
||||||
text={<>
|
text={<>
|
||||||
{isVerticalLayout && <VerticalLayoutIcon />}
|
{isVerticalLayout && <VerticalLayoutIcon logoRef={logoRef} />}
|
||||||
{isUpdateAvailable && <div class="global-menu-button-update-available">
|
{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")} />
|
<span className="bx bxs-down-arrow-alt global-menu-button-update-available-button" title={t("update_available.update_available")} />
|
||||||
</div>}
|
</div>}
|
||||||
@ -135,9 +137,9 @@ function SwitchToOptions() {
|
|||||||
return;
|
return;
|
||||||
} else if (!isMobile()) {
|
} else if (!isMobile()) {
|
||||||
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />;
|
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />;
|
||||||
}
|
}
|
||||||
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />;
|
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
|
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
|
||||||
@ -159,10 +161,7 @@ function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<Keybo
|
|||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function VerticalLayoutIcon() {
|
export function VerticalLayoutIcon({ logoRef }: { logoRef?: RefObject<SVGSVGElement> }) {
|
||||||
const logoRef = useRef<SVGSVGElement>(null);
|
|
||||||
useStaticTooltip(logoRef);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
|
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
|
||||||
<g>
|
<g>
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import Modal from "../react/Modal";
|
import Modal from "../react/Modal";
|
||||||
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
import { dismissCallToAction, getCallToActions } from "./call_to_action_definitions";
|
||||||
import { t } from "../../services/i18n";
|
|
||||||
|
|
||||||
export default function CallToActionDialog() {
|
export default function CallToActionDialog() {
|
||||||
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
const activeCallToActions = useMemo(() => getCallToActions(), []);
|
||||||
const [ activeIndex, setActiveIndex ] = useState(0);
|
const [ activeIndex, setActiveIndex ] = useState(0);
|
||||||
const [ shown, setShown ] = useState(true);
|
const [ shown, setShown ] = useState(true);
|
||||||
const activeItem = activeCallToActions[activeIndex];
|
const activeItem = activeCallToActions[activeIndex];
|
||||||
@ -36,11 +37,11 @@ export default function CallToActionDialog() {
|
|||||||
await dismissCallToAction(activeItem.id);
|
await dismissCallToAction(activeItem.id);
|
||||||
await button.onClick();
|
await button.onClick();
|
||||||
goToNext();
|
goToNext();
|
||||||
}}/>
|
}}/>
|
||||||
)}
|
)}
|
||||||
</>}
|
</>}
|
||||||
>
|
>
|
||||||
<p>{activeItem.message}</p>
|
<p className="pre-wrap-text">{activeItem.message}</p>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import utils from "../../services/utils";
|
|
||||||
import options from "../../services/options";
|
|
||||||
import { t } from "../../services/i18n";
|
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.
|
* 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[] = [
|
const CALL_TO_ACTIONS: CallToAction[] = [
|
||||||
{
|
{
|
||||||
id: "next_theme",
|
id: "new_layout",
|
||||||
title: t("call_to_action.next_theme_title"),
|
title: t("call_to_action.new_layout_title"),
|
||||||
message: t("call_to_action.next_theme_message"),
|
message: t("call_to_action.new_layout_message"),
|
||||||
enabled: () => !isNextTheme(),
|
enabled: () => true,
|
||||||
buttons: [
|
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: "background_effects",
|
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 {
|
.inline-title {
|
||||||
margin-top: 2px; /* Allow space for the focus outline */
|
|
||||||
max-width: var(--max-content-width);
|
max-width: var(--max-content-width);
|
||||||
container-type: inline-size;
|
container-type: inline-size;
|
||||||
padding-inline-start: 24px;
|
padding-inline-start: 24px;
|
||||||
@ -111,7 +110,7 @@ body.prefers-centered-content .inline-title {
|
|||||||
|
|
||||||
.note-type-switcher {
|
.note-type-switcher {
|
||||||
--badge-radius: 12px;
|
--badge-radius: 12px;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 5px;
|
top: 5px;
|
||||||
padding: .25em 0;
|
padding: .25em 0;
|
||||||
@ -121,7 +120,7 @@ body.prefers-centered-content .inline-title {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
min-height: 35px;
|
min-height: 35px;
|
||||||
|
|
||||||
>* {
|
>* {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
animation: note-type-switcher-intro 200ms ease-in;
|
animation: note-type-switcher-intro 200ms ease-in;
|
||||||
|
|||||||
@ -22,11 +22,10 @@ export default function NoteBadges() {
|
|||||||
|
|
||||||
function ReadOnlyBadge() {
|
function ReadOnlyBadge() {
|
||||||
const { note, noteContext } = useNoteContext();
|
const { note, noteContext } = useNoteContext();
|
||||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
const { isReadOnly, enableEditing, temporarilyEditable } = useIsNoteReadOnly(note, noteContext);
|
||||||
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
|
||||||
const isTemporarilyEditable = noteContext?.ntxId !== "_popup-editor" && noteContext?.viewScope?.readOnlyTemporarilyDisabled;
|
|
||||||
|
|
||||||
if (isTemporarilyEditable) {
|
if (temporarilyEditable) {
|
||||||
return <Badge
|
return <Badge
|
||||||
icon="bx bx-lock-open-alt"
|
icon="bx bx-lock-open-alt"
|
||||||
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
|
text={t("breadcrumb_badges.read_only_temporarily_disabled")}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "./NoteTitleActions.css";
|
import "./NoteTitleActions.css";
|
||||||
|
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
import NoteContext from "../../components/note_context";
|
import NoteContext from "../../components/note_context";
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
@ -10,7 +10,7 @@ import CollectionProperties from "../note_bars/CollectionProperties";
|
|||||||
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
|
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
|
||||||
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
|
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
|
||||||
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
|
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
|
||||||
import { useNoteContext, useNoteProperty } from "../react/hooks";
|
import { useNoteContext, useNoteProperty, useTriliumEvent } from "../react/hooks";
|
||||||
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
|
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
|
||||||
|
|
||||||
export default function NoteTitleActions() {
|
export default function NoteTitleActions() {
|
||||||
@ -57,6 +57,9 @@ function PromotedAttributes({ note, componentId, noteContext }: {
|
|||||||
});
|
});
|
||||||
}, [ note, noteContext ]);
|
}, [ note, noteContext ]);
|
||||||
|
|
||||||
|
// Keyboard shortcut.
|
||||||
|
useTriliumEvent("toggleRibbonTabPromotedAttributes", () => setExpanded(!expanded));
|
||||||
|
|
||||||
if (!cells?.length) return false;
|
if (!cells?.length) return false;
|
||||||
return (note && (
|
return (note && (
|
||||||
<ExternallyControlledCollapsible
|
<ExternallyControlledCollapsible
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import "./StatusBar.css";
|
import "./StatusBar.css";
|
||||||
|
|
||||||
import { Locale } from "@triliumnext/commons";
|
import { KeyboardActionNames, Locale } from "@triliumnext/commons";
|
||||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { type ComponentChildren } from "preact";
|
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 { CommandNames } from "../../components/app_context";
|
||||||
import NoteContext from "../../components/note_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 attributes from "../../services/attributes";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ViewScope } from "../../services/link";
|
import { ViewScope } from "../../services/link";
|
||||||
@ -216,14 +216,19 @@ interface NoteInfoContext extends StatusBarContext {
|
|||||||
setSimilarNotesShown: (value: boolean) => void;
|
setSimilarNotesShown: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NoteInfoBadge({ note, setSimilarNotesShown }: NoteInfoContext) {
|
export function NoteInfoBadge({ note, similarNotesShown, setSimilarNotesShown }: NoteInfoContext) {
|
||||||
const dropdownRef = useRef<BootstrapDropdown>(null);
|
const dropdownRef = useRef<BootstrapDropdown>(null);
|
||||||
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
||||||
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
|
||||||
const currentNoteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
const currentNoteTypeData = useMemo(() => NOTE_TYPES.find(t => t.type === currentNoteType), [ currentNoteType ]);
|
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
|
<StatusBarDropdown
|
||||||
icon="bx bx-info-circle"
|
icon="bx bx-info-circle"
|
||||||
title={t("status_bar.note_info_title")}
|
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} />}
|
{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.created")} value={formatDateTime(metadata?.dateCreated)} />
|
||||||
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
|
<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} />}
|
{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_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} />} />
|
<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.
|
// Show on keyboard shortcuts.
|
||||||
useTriliumEvents([ "addNewLabel", "addNewRelation" ], () => setAttributesShown(true));
|
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.
|
// Interaction with the attribute editor.
|
||||||
useLegacyImperativeHandlers(useMemo(() => ({
|
useLegacyImperativeHandlers(useMemo(() => ({
|
||||||
@ -373,12 +382,18 @@ function AttributesPane({ note, noteContext, attributesShown, setAttributesShown
|
|||||||
|
|
||||||
//#region Note paths
|
//#region Note paths
|
||||||
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
|
function NotePaths({ note, hoistedNoteId, notePath }: StatusBarContext) {
|
||||||
|
const dropdownRef = useRef<BootstrapDropdown>(null);
|
||||||
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
|
||||||
const count = sortedNotePaths?.length ?? 0;
|
const count = sortedNotePaths?.length ?? 0;
|
||||||
|
const enabled = count > 1;
|
||||||
|
|
||||||
return (count > 1 &&
|
// Keyboard shortcut.
|
||||||
|
useTriliumEvent("toggleRibbonTabNotePaths", () => enabled && dropdownRef.current?.show());
|
||||||
|
|
||||||
|
return (enabled &&
|
||||||
<StatusBarDropdown
|
<StatusBarDropdown
|
||||||
title={t("status_bar.note_paths_title")}
|
title={t("status_bar.note_paths_title")}
|
||||||
|
dropdownRef={dropdownRef}
|
||||||
dropdownContainerClassName="dropdown-note-paths"
|
dropdownContainerClassName="dropdown-note-paths"
|
||||||
icon="bx bx-directions"
|
icon="bx bx-directions"
|
||||||
text={t("status_bar.note_paths", { count })}
|
text={t("status_bar.note_paths", { count })}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import "./CollectionProperties.css";
|
import "./CollectionProperties.css";
|
||||||
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext, useRef } from "preact/hooks";
|
||||||
import { Fragment } from "preact/jsx-runtime";
|
import { Fragment } from "preact/jsx-runtime";
|
||||||
|
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
@ -12,7 +12,7 @@ import ActionButton from "../react/ActionButton";
|
|||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
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 Icon from "../react/Icon";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
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 }) {
|
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 (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
dropdownContainerRef={dropdownContainerRef}
|
||||||
text={<>
|
text={<>
|
||||||
<Icon icon={ICON_MAPPINGS[viewType]} />
|
<Icon icon={ICON_MAPPINGS[viewType]} />
|
||||||
{VIEW_TYPE_MAPPINGS[viewType]}
|
{VIEW_TYPE_MAPPINGS[viewType]}
|
||||||
|
|||||||
@ -2,13 +2,13 @@ import "./FormList.css";
|
|||||||
|
|
||||||
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren, RefObject } from "preact";
|
||||||
import { type CSSProperties,useEffect, useMemo, useRef, useState } from "preact/compat";
|
import { type CSSProperties,useEffect, useMemo, useRef, useState } from "preact/compat";
|
||||||
|
|
||||||
import { CommandNames } from "../../components/app_context";
|
import { CommandNames } from "../../components/app_context";
|
||||||
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
|
import { handleRightToLeftPlacement, isMobile, openInAppHelpFromUrl } from "../../services/utils";
|
||||||
import FormToggle from "./FormToggle";
|
import FormToggle from "./FormToggle";
|
||||||
import { useStaticTooltip } from "./hooks";
|
import { useStaticTooltip, useSyncedRef } from "./hooks";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
interface FormListOpts {
|
interface FormListOpts {
|
||||||
@ -97,6 +97,7 @@ interface FormListItemOpts {
|
|||||||
className?: string;
|
className?: string;
|
||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
postContent?: ComponentChildren;
|
postContent?: ComponentChildren;
|
||||||
|
itemRef?: RefObject<HTMLLIElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
||||||
@ -104,8 +105,8 @@ const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
|||||||
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
|
fallbackPlacements: [ handleRightToLeftPlacement("right") ]
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
|
export function FormListItem({ className, icon, value, title, active, disabled, checked, container, onClick, selected, rtl, triggerCommand, description, itemRef: externalItemRef, ...contentProps }: FormListItemOpts) {
|
||||||
const itemRef = useRef<HTMLLIElement>(null);
|
const itemRef = useSyncedRef<HTMLLIElement>(externalItemRef, null);
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
icon = "bx bx-check";
|
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 [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
|
||||||
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
|
const [ readOnlyAttr ] = useNoteLabelBoolean(note, "readOnly");
|
||||||
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
|
const [ autoReadOnlyDisabledAttr ] = useNoteLabelBoolean(note, "autoReadOnlyDisabled");
|
||||||
|
const [ temporarilyEditable, setTemporarilyEditable ] = useState(false);
|
||||||
|
|
||||||
const enableEditing = useCallback((enabled = true) => {
|
const enableEditing = useCallback((enabled = true) => {
|
||||||
if (noteContext?.viewScope) {
|
if (noteContext?.viewScope) {
|
||||||
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
|
noteContext.viewScope.readOnlyTemporarilyDisabled = enabled;
|
||||||
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
appContext.triggerEvent("readOnlyTemporarilyDisabled", {noteContext});
|
||||||
|
setTemporarilyEditable(enabled);
|
||||||
}
|
}
|
||||||
}, [noteContext]);
|
}, [noteContext]);
|
||||||
|
|
||||||
@ -945,6 +947,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
|||||||
if (note && noteContext) {
|
if (note && noteContext) {
|
||||||
isNoteReadOnly(note, noteContext).then((readOnly) => {
|
isNoteReadOnly(note, noteContext).then((readOnly) => {
|
||||||
setIsReadOnly(readOnly);
|
setIsReadOnly(readOnly);
|
||||||
|
setTemporarilyEditable(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
|
}, [ note, noteContext, noteContext?.viewScope, readOnlyAttr, autoReadOnlyDisabledAttr ]);
|
||||||
@ -952,10 +955,11 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
|
|||||||
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
|
||||||
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
if (noteContext?.ntxId === eventNoteContext.ntxId) {
|
||||||
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
|
setIsReadOnly(!noteContext.viewScope?.readOnlyTemporarilyDisabled);
|
||||||
|
setTemporarilyEditable(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { isReadOnly, enableEditing };
|
return { isReadOnly, enableEditing, temporarilyEditable };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||||
|
|||||||
@ -49,6 +49,21 @@ export function FixedFormattingToolbar() {
|
|||||||
const renderState = useRenderState(noteContext, note);
|
const renderState = useRenderState(noteContext, note);
|
||||||
const [ toolbarToRender, setToolbarToRender ] = useState<HTMLElement | null | undefined>();
|
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.
|
// Populate the cache with the toolbar of every note context.
|
||||||
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
||||||
if (!eventNtxId) return;
|
if (!eventNtxId) return;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
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 appContext, { CommandNames } from "../../components/app_context";
|
||||||
import Component from "../../components/component";
|
import Component from "../../components/component";
|
||||||
@ -20,7 +22,7 @@ import MovePaneButton from "../buttons/move_pane_button";
|
|||||||
import ActionButton from "../react/ActionButton";
|
import ActionButton from "../react/ActionButton";
|
||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
|
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 { ParentComponent } from "../react/react_utils";
|
||||||
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
|
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
|
||||||
import NoteActionsCustom from "./NoteActionsCustom";
|
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 }) {
|
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
|
||||||
|
const dropdownRef = useRef<BootstrapDropdown>(null);
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
const noteType = useNoteProperty(note, "type") ?? "";
|
const noteType = useNoteProperty(note, "type") ?? "";
|
||||||
const [viewType] = useNoteLabel(note, "viewType");
|
const [viewType] = useNoteLabel(note, "viewType");
|
||||||
@ -77,14 +82,25 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
|||||||
const [syncServerHost] = useTriliumOption("syncServerHost");
|
const [syncServerHost] = useTriliumOption("syncServerHost");
|
||||||
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
|
||||||
const isNormalViewMode = noteContext?.viewScope?.viewMode === "default";
|
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 (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
dropdownRef={dropdownRef}
|
||||||
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
|
||||||
className="note-actions"
|
className="note-actions"
|
||||||
hideToggleArrow
|
hideToggleArrow
|
||||||
noSelectButtonStyle
|
noSelectButtonStyle
|
||||||
iconAction>
|
iconAction
|
||||||
|
onHidden={() => itemToFocusRef.current = null }
|
||||||
|
>
|
||||||
|
|
||||||
{isReadOnly && <>
|
{isReadOnly && <>
|
||||||
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
|
<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 />
|
<FormDropdownDivider />
|
||||||
|
|
||||||
{isNewLayout && isNormalViewMode && !isHelpPage && <>
|
{isNewLayout && isNormalViewMode && !isHelpPage && <>
|
||||||
<NoteBasicProperties note={note} />
|
<NoteBasicProperties note={note} focus={itemToFocusRef} />
|
||||||
<FormDropdownDivider />
|
<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 [ isBookmarked, setIsBookmarked ] = useNoteBookmarkState(note);
|
||||||
const [ isShared, switchShareState ] = useShareState(note);
|
const [ isShared, switchShareState ] = useShareState(note);
|
||||||
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
|
const [ isTemplate, setIsTemplate ] = useNoteLabelBoolean(note, "template");
|
||||||
const isProtected = useNoteProperty(note, "isProtected");
|
const isProtected = useNoteProperty(note, "isProtected");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focus.current === "basic-properties") {
|
||||||
|
itemToFocusRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [ focus ]);
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<FormListToggleableItem
|
<FormListToggleableItem
|
||||||
icon="bx bx-share-alt"
|
icon="bx bx-share-alt"
|
||||||
@ -161,6 +187,7 @@ function NoteBasicProperties({ note }: { note: FNote }) {
|
|||||||
currentValue={isShared} onChange={switchShareState}
|
currentValue={isShared} onChange={switchShareState}
|
||||||
helpPage="R9pX4DGra2Vt"
|
helpPage="R9pX4DGra2Vt"
|
||||||
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
|
disabled={["root", "_share", "_hidden"].includes(note?.noteId ?? "") || note?.noteId.startsWith("_options")}
|
||||||
|
itemRef={itemToFocusRef}
|
||||||
/>
|
/>
|
||||||
<FormListToggleableItem
|
<FormListToggleableItem
|
||||||
icon="bx bx-lock-alt"
|
icon="bx bx-lock-alt"
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { NoteType } from "@triliumnext/commons";
|
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 Component from "../../components/component";
|
||||||
import NoteContext from "../../components/note_context";
|
import NoteContext from "../../components/note_context";
|
||||||
@ -12,7 +12,7 @@ import { ViewTypeOptions } from "../collections/interface";
|
|||||||
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
|
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
|
||||||
import ActionButton from "../react/ActionButton";
|
import ActionButton from "../react/ActionButton";
|
||||||
import { FormFileUploadActionButton } from "../react/FormFileUpload";
|
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 { ParentComponent } from "../react/react_utils";
|
||||||
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
|
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
|
||||||
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
|
import { buildUploadNewImageRevisionListener } from "./ImagePropertiesTab";
|
||||||
@ -38,6 +38,7 @@ interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
|
|||||||
*/
|
*/
|
||||||
export default function NoteActionsCustom(props: NoteActionsCustomProps) {
|
export default function NoteActionsCustom(props: NoteActionsCustomProps) {
|
||||||
const { note } = props;
|
const { note } = props;
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const noteType = useNoteProperty(note, "type");
|
const noteType = useNoteProperty(note, "type");
|
||||||
const noteMime = useNoteProperty(note, "mime");
|
const noteMime = useNoteProperty(note, "mime");
|
||||||
const [ viewType ] = useNoteLabel(note, "viewType");
|
const [ viewType ] = useNoteLabel(note, "viewType");
|
||||||
@ -53,8 +54,15 @@ export default function NoteActionsCustom(props: NoteActionsCustomProps) {
|
|||||||
isReadOnly
|
isReadOnly
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useTriliumEvents([ "toggleRibbonTabFileProperties", "toggleRibbonTabImageProperties" ], () => {
|
||||||
|
(containerRef.current?.firstElementChild as HTMLElement)?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
return (innerProps &&
|
return (innerProps &&
|
||||||
<div className="note-actions-custom">
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className="note-actions-custom"
|
||||||
|
>
|
||||||
<AddChildButton {...innerProps} />
|
<AddChildButton {...innerProps} />
|
||||||
<RunActiveNoteButton {...innerProps } />
|
<RunActiveNoteButton {...innerProps } />
|
||||||
<OpenTriliumApiDocsButton {...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 { 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 { t } from "../../../services/i18n";
|
||||||
import server from "../../../services/server";
|
import link from "../../../services/link";
|
||||||
import note_autocomplete, { Suggestion } from "../../../services/note_autocomplete";
|
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 CKEditor, { CKEditorApi } from "../../react/CKEditor";
|
||||||
import { useLegacyImperativeHandlers, useLegacyWidget, useTooltip, useTriliumEvent, useTriliumOption } from "../../react/hooks";
|
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>;
|
type AttributeCommandNames = FilteredCommandNames<CommandData>;
|
||||||
|
|
||||||
@ -52,7 +53,7 @@ const mentionSetup: MentionFeed[] = [
|
|||||||
return names.map((name) => {
|
return names.map((name) => {
|
||||||
return {
|
return {
|
||||||
id: `#${name}`,
|
id: `#${name}`,
|
||||||
name: name
|
name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -66,7 +67,7 @@ const mentionSetup: MentionFeed[] = [
|
|||||||
return names.map((name) => {
|
return names.map((name) => {
|
||||||
return {
|
return {
|
||||||
id: `~${name}`,
|
id: `~${name}`,
|
||||||
name: name
|
name
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -85,9 +86,10 @@ interface AttributeEditorProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AttributeEditorImperativeHandlers {
|
export interface AttributeEditorImperativeHandlers {
|
||||||
save: () => Promise<void>;
|
save(): Promise<void>;
|
||||||
refresh: () => void;
|
refresh(): void;
|
||||||
renderOwnedAttributes: (ownedAttributes: FAttribute[]) => Promise<void>;
|
focus(): void;
|
||||||
|
renderOwnedAttributes(ownedAttributes: FAttribute[]): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AttributeEditor({ api, note, componentId, notePath, ntxId, hidden }: AttributeEditorProps) {
|
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
|
// attrs are not resorted if position changes after the initial load
|
||||||
ownedAttributes.sort((a, b) => a.position - b.position);
|
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) {
|
if (saved) {
|
||||||
lastSavedContent.current = htmlAttrs;
|
lastSavedContent.current = htmlAttrs;
|
||||||
@ -162,7 +164,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
wrapperRef.current.style.opacity = "0";
|
wrapperRef.current.style.opacity = "0";
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (wrapperRef.current) {
|
if (wrapperRef.current) {
|
||||||
wrapperRef.current.style.opacity = "1"
|
wrapperRef.current.style.opacity = "1";
|
||||||
}
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
@ -252,7 +254,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
if (notePath) {
|
if (notePath) {
|
||||||
result = await note_create.createNoteWithTypePrompt(notePath, {
|
result = await note_create.createNoteWithTypePrompt(notePath, {
|
||||||
activate: false,
|
activate: false,
|
||||||
title: title
|
title
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +276,8 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
useImperativeHandle(api, () => ({
|
useImperativeHandle(api, () => ({
|
||||||
save,
|
save,
|
||||||
refresh,
|
refresh,
|
||||||
renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false)
|
renderOwnedAttributes: (attributes) => renderOwnedAttributes(attributes as FAttribute[], false),
|
||||||
|
focus: () => editorRef.current?.focus()
|
||||||
}), [ save, refresh, renderOwnedAttributes ]);
|
}), [ save, refresh, renderOwnedAttributes ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -404,7 +407,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
|||||||
|
|
||||||
{attributeDetailWidgetEl}
|
{attributeDetailWidgetEl}
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPreprocessedData(currentValue: string) {
|
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 { useEffect, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { t } from "../../../services/i18n";
|
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 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 FormCheckbox from "../../react/FormCheckbox";
|
||||||
import FormGroup from "../../react/FormGroup";
|
import FormGroup from "../../react/FormGroup";
|
||||||
import { FontFamily, OptionNames } from "@triliumnext/commons";
|
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
import FormSelect, { FormSelectWithGroups } from "../../react/FormSelect";
|
||||||
import FormText from "../../react/FormText";
|
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";
|
import RelatedSettings from "./components/RelatedSettings";
|
||||||
|
|
||||||
const MIN_CONTENT_WIDTH = 640;
|
const MIN_CONTENT_WIDTH = 640;
|
||||||
@ -30,7 +36,7 @@ const BUILTIN_THEMES: Theme[] = [
|
|||||||
{ val: "auto", title: t("theme.auto_theme") },
|
{ val: "auto", title: t("theme.auto_theme") },
|
||||||
{ val: "light", title: t("theme.light_theme") },
|
{ val: "light", title: t("theme.light_theme") },
|
||||||
{ val: "dark", title: t("theme.dark_theme") }
|
{ val: "dark", title: t("theme.dark_theme") }
|
||||||
]
|
];
|
||||||
|
|
||||||
interface FontFamilyEntry {
|
interface FontFamilyEntry {
|
||||||
value: FontFamily;
|
value: FontFamily;
|
||||||
@ -84,6 +90,7 @@ export default function AppearanceSettings() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{!isMobile() && <LayoutSwitcher />}
|
||||||
{!isMobile() && <LayoutOrientation />}
|
{!isMobile() && <LayoutOrientation />}
|
||||||
<ApplicationTheme />
|
<ApplicationTheme />
|
||||||
{overrideThemeFonts === "true" && <Fonts />}
|
{overrideThemeFonts === "true" && <Fonts />}
|
||||||
@ -102,7 +109,99 @@ export default function AppearanceSettings() {
|
|||||||
}
|
}
|
||||||
]} />
|
]} />
|
||||||
</div>
|
</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() {
|
function LayoutOrientation() {
|
||||||
@ -141,7 +240,7 @@ function ApplicationTheme() {
|
|||||||
setThemes([
|
setThemes([
|
||||||
...BUILTIN_THEMES,
|
...BUILTIN_THEMES,
|
||||||
...userThemes
|
...userThemes
|
||||||
])
|
]);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -162,7 +261,7 @@ function ApplicationTheme() {
|
|||||||
</FormGroup>
|
</FormGroup>
|
||||||
</div>
|
</div>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Fonts() {
|
function Fonts() {
|
||||||
@ -245,7 +344,7 @@ function ElectronIntegration() {
|
|||||||
|
|
||||||
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
|
<Button text={t("electron_integration.restart-app-button")} onClick={restartDesktopApp} />
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Performance() {
|
function Performance() {
|
||||||
@ -271,7 +370,7 @@ function Performance() {
|
|||||||
|
|
||||||
{isElectron() && <SmoothScrollEnabledOption />}
|
{isElectron() && <SmoothScrollEnabledOption />}
|
||||||
|
|
||||||
</OptionsSection>
|
</OptionsSection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SmoothScrollEnabledOption() {
|
function SmoothScrollEnabledOption() {
|
||||||
@ -280,7 +379,7 @@ function SmoothScrollEnabledOption() {
|
|||||||
return <FormCheckbox
|
return <FormCheckbox
|
||||||
label={`${t("ui-performance.enable-smooth-scroll")} ${t("ui-performance.app-restart-required")}`}
|
label={`${t("ui-performance.enable-smooth-scroll")} ${t("ui-performance.app-restart-required")}`}
|
||||||
currentValue={smoothScrollEnabled} onChange={setSmoothScrollEnabled}
|
currentValue={smoothScrollEnabled} onChange={setSmoothScrollEnabled}
|
||||||
/>
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MaxContentWidth() {
|
function MaxContentWidth() {
|
||||||
@ -302,10 +401,10 @@ function MaxContentWidth() {
|
|||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<FormCheckbox label={t("max_content_width.centerContent")}
|
<FormCheckbox label={t("max_content_width.centerContent")}
|
||||||
currentValue={centerContent}
|
currentValue={centerContent}
|
||||||
onChange={setCenterContent} />
|
onChange={setCenterContent} />
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RibbonOptions() {
|
function RibbonOptions() {
|
||||||
@ -318,5 +417,5 @@ function RibbonOptions() {
|
|||||||
currentValue={editedNotesOpenInRibbon} onChange={setEditedNotesOpenInRibbon}
|
currentValue={editedNotesOpenInRibbon} onChange={setEditedNotesOpenInRibbon}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
.option-row > label {
|
.option-row > label {
|
||||||
width: 40%;
|
width: 40%;
|
||||||
margin-bottom: 0 !important;
|
margin-bottom: 0 !important;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-row > select,
|
.option-row > select,
|
||||||
@ -26,4 +27,4 @@
|
|||||||
|
|
||||||
.option-row.centered {
|
.option-row.centered {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 { OptionNames } from "@triliumnext/commons";
|
||||||
|
import type { Request } from "express";
|
||||||
|
|
||||||
|
import ValidationError from "../../errors/validation_error.js";
|
||||||
import config from "../../services/config.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 {
|
interface UserTheme {
|
||||||
val: string; // value of the theme, used in the URL
|
val: string; // value of the theme, used in the URL
|
||||||
@ -100,6 +101,7 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
|||||||
"splitEditorOrientation",
|
"splitEditorOrientation",
|
||||||
"seenCallToActions",
|
"seenCallToActions",
|
||||||
"experimentalFeatures",
|
"experimentalFeatures",
|
||||||
|
"newLayout",
|
||||||
|
|
||||||
// AI/LLM integration options
|
// AI/LLM integration options
|
||||||
"aiEnabled",
|
"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 appInfo from "./app_info.js";
|
||||||
import { randomSecureToken, isWindows } from "./utils.js";
|
|
||||||
import log from "./log.js";
|
|
||||||
import dateUtils from "./date_utils.js";
|
import dateUtils from "./date_utils.js";
|
||||||
import keyboardActions from "./keyboard_actions.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() {
|
function initDocumentOptions() {
|
||||||
optionService.createOption("documentId", randomSecureToken(16), false);
|
optionService.createOption("documentId", randomSecureToken(16), false);
|
||||||
@ -156,6 +157,7 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
{ name: "shadowsEnabled", value: "true", isSynced: false },
|
{ name: "shadowsEnabled", value: "true", isSynced: false },
|
||||||
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
|
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
|
||||||
{ name: "smoothScrollEnabled", value: "true", isSynced: false },
|
{ name: "smoothScrollEnabled", value: "true", isSynced: false },
|
||||||
|
{ name: "newLayout", value: "true", isSynced: true },
|
||||||
|
|
||||||
// Internationalization
|
// Internationalization
|
||||||
{ name: "locale", value: "en", isSynced: true },
|
{ name: "locale", value: "en", isSynced: true },
|
||||||
@ -171,9 +173,9 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
value: (optionsMap) => {
|
value: (optionsMap) => {
|
||||||
if (optionsMap.theme === "light") {
|
if (optionsMap.theme === "light") {
|
||||||
return "default:stackoverflow-light";
|
return "default:stackoverflow-light";
|
||||||
} else {
|
|
||||||
return "default:stackoverflow-dark";
|
|
||||||
}
|
}
|
||||||
|
return "default:stackoverflow-dark";
|
||||||
|
|
||||||
},
|
},
|
||||||
isSynced: false
|
isSynced: false
|
||||||
},
|
},
|
||||||
|
|||||||
@ -131,6 +131,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
|||||||
/** Whether keyboard auto-completion for editing commands is triggered when typing `/`. */
|
/** Whether keyboard auto-completion for editing commands is triggered when typing `/`. */
|
||||||
textNoteSlashCommandsEnabled: boolean;
|
textNoteSlashCommandsEnabled: boolean;
|
||||||
backgroundEffects: boolean;
|
backgroundEffects: boolean;
|
||||||
|
newLayout: boolean;
|
||||||
|
|
||||||
// Share settings
|
// Share settings
|
||||||
redirectBareDomain: boolean;
|
redirectBareDomain: boolean;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user