Compare commits

...

42 Commits

Author SHA1 Message Date
Wael Nasreddine
09c7fb095d
Merge 07f273fda40b138c0262d8c1aea172948af62855 into f0b30c5e91060381a6b2cc2976baa95f45a1bc4f 2026-02-05 23:44:42 +02:00
Elian Doran
f0b30c5e91
Mobile improvements v2 (#8615)
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
Deploy Documentation / Build and Deploy Documentation (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
2026-02-05 23:44:31 +02:00
Elian Doran
73e3196124
docs(user): improve documentation on mobile support 2026-02-05 23:31:12 +02:00
Elian Doran
8a7bcc316e
chore(mobile): address requested changes
Some checks failed
Checks / main (push) Has been cancelled
2026-02-05 22:25:01 +02:00
Elian Doran
a2921cb982
fix(mobile): missing snap in board view 2026-02-05 22:15:41 +02:00
Elian Doran
29ce004974
chore(mobile): add possible work-around for note switcher sometimes disappearing 2026-02-05 22:13:09 +02:00
Elian Doran
026ba5ddce
chore(mobile/attachments): improve layout 2026-02-05 22:02:11 +02:00
Elian Doran
ab0585609a
chore(mobile/attachments): use bottom-style menu for actions 2026-02-05 21:57:49 +02:00
Elian Doran
14a8bdb0c0
chore(launch_bar): add backdrop on mobile for dropdown menus 2026-02-05 21:53:54 +02:00
Elian Doran
397d04dd88
style(client): fix rounded corners in launcher bar dropdown 2026-02-05 21:48:38 +02:00
Elian Doran
fbb0bb7491
Revert "feat(mobile): make the sidebar gesture easier to press"
This reverts commit f8b386e42d0adbb7c4f927216aa668c8c62b4535.
2026-02-05 21:47:40 +02:00
Elian Doran
ee987dae99
refactor(client): fix typo in container 2026-02-05 21:44:48 +02:00
Elian Doran
720281a8db
feat(bookmarks): support bookmark folders on mobile 2026-02-05 21:43:58 +02:00
Elian Doran
ff0c89e5a3
feat(bookmarks): collapse on mobile into single icon (closes #5464) 2026-02-05 21:28:50 +02:00
Elian Doran
442937f540
feat(bookmarks): add launcher on mobile 2026-02-05 21:20:22 +02:00
Elian Doran
dc01b787c1
fix(launch_bar): cannot create new items in mobile launch bar (fixes #8054) 2026-02-05 21:10:05 +02:00
Elian Doran
c709b5d34c
chore(mobile): relocate some note actions 2026-02-05 18:42:51 +02:00
Elian Doran
f8b386e42d
feat(mobile): make the sidebar gesture easier to press 2026-02-05 16:43:06 +02:00
Elian Doran
a82f8ce3ad
Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2 2026-02-05 10:28:02 +02:00
Elian Doran
d31135bf21
fix(mobile): status bar color for Samsung Internet
Some checks are pending
Checks / main (push) Waiting to run
2026-02-05 09:09:47 +02:00
Elian Doran
75a77acefe
Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2 2026-02-05 09:09:41 +02:00
Elian Doran
ec915177ad
feat(mobile): add search to launch bar 2026-02-04 21:36:00 +02:00
Elian Doran
5adee3e217
fix(mobile/search): missing rounded corners for bulk actions 2026-02-04 21:31:23 +02:00
Elian Doran
278d82645e
feat(mobile): use safe area for bottom-aligned menus 2026-02-04 21:15:08 +02:00
Elian Doran
e66f13b471
feat(mobile/search): use bottom menus for large dropdowns 2026-02-04 21:11:43 +02:00
Elian Doran
0e2955b57e
feat(mobile/search): group search buttons into split 2026-02-04 21:06:20 +02:00
Elian Doran
2a4d5ec1ec
feat(mobile/search): group search options in dropdown 2026-02-04 20:55:55 +02:00
Elian Doran
07ce63de69
chore(mobile/search): remove redundant margin 2026-02-04 20:44:15 +02:00
Elian Doran
b539862eef
fix(mobile/search): duplicate search parameters 2026-02-04 20:41:36 +02:00
Elian Doran
ac4be3f8a8
feat(mobile/global_menu): add option to search notes 2026-02-04 20:40:31 +02:00
Elian Doran
0dc2d07b58
fix(mobile): virtual keyboard detection not working on iOS 2026-02-04 19:09:18 +02:00
Elian Doran
b097f9dc21
fix(mobile): fixed tree overflowing container 2026-02-04 18:53:32 +02:00
Elian Doran
8aa4a97480
fix(mobile): fixed tree for launcher duplicated in split 2026-02-04 18:52:46 +02:00
Elian Doran
3e54d0ceae
chore(hidden_tree): add icon to mobile tab switcher launcher 2026-02-04 18:48:37 +02:00
Elian Doran
e1cec3404a
Merge remote-tracking branch 'origin/main' into feature/mobile_improvements_v2
; Conflicts:
;	apps/client/src/widgets/mobile_widgets/mobile_detail_menu.tsx
2026-02-04 18:46:38 +02:00
Elian Doran
e00e3999c5
feat(mobile/note_actions): indicate current content language 2026-02-04 18:05:37 +02:00
Elian Doran
2cbe96d815
feat(mobile/note_actions): integrate text content language switcher 2026-02-04 18:03:17 +02:00
Elian Doran
0d8453f6a7
feat(mobile/note_actions): integrate code language switcher 2026-02-03 18:58:04 +02:00
Elian Doran
52b41b1bb0
feat(mobile/note_actions): integrate similar notes 2026-02-03 18:18:57 +02:00
Elian Doran
c7265017b3
feat(mobile/note_actions): integrate styling for note info 2026-02-03 18:09:02 +02:00
Elian Doran
634e0b6d30
feat(mobile/note_actions): integrate note info 2026-02-03 18:04:08 +02:00
Wael Nasreddine
07f273fda4 feat: enhance build-docs utility with pnpm and Nix support
This enhances the `build-docs` utility to allow running it via pnpm and
packaging it as a Nix application. Changes include:

- Added `build` and `cli` scripts to `apps/build-docs/package.json`.
- Implemented a standalone CLI wrapper for documentation generation.
- Added a `trilium-build-docs` package to the Nix flake for use in other projects.
- Integrated `apps/build-docs` into the Nix workspace and build pipeline.

These changes make the documentation build process more portable and easier
to integrate into CI/CD pipelines outside of the main repository.
2026-02-01 00:32:51 -08:00
63 changed files with 1399 additions and 500 deletions

View File

@ -1,10 +1,15 @@
{
"name": "build-docs",
"version": "1.0.0",
"description": "",
"description": "Build documentation from Trilium notes",
"main": "src/main.ts",
"bin": {
"trilium-build-docs": "dist/cli.js"
},
"scripts": {
"start": "tsx ."
"start": "tsx .",
"cli": "tsx src/cli.ts",
"build": "tsx scripts/build.ts"
},
"keywords": [],
"author": "Elian Doran <contact@eliandoran.me>",
@ -14,6 +19,7 @@
"@redocly/cli": "2.15.1",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"js-yaml": "4.1.1",
"react": "19.2.4",
"react-dom": "19.2.4",
"typedoc": "0.28.16",

View File

@ -0,0 +1,23 @@
import BuildHelper from "../../../scripts/build-utils";
const build = new BuildHelper("apps/build-docs");
async function main() {
// Build the CLI and other TypeScript files
await build.buildBackend([
"src/cli.ts",
"src/main.ts",
"src/build-docs.ts",
"src/swagger.ts",
"src/script-api.ts",
"src/context.ts"
]);
// Copy HTML template
build.copy("src/index.html", "index.html");
// Copy node modules dependencies if needed
build.copyNodeModules([ "better-sqlite3", "bindings", "file-uri-to-path" ]);
}
main();

View File

@ -13,8 +13,12 @@
* Make sure to keep in line with backend's `script_context.ts`.
*/
export type { default as AbstractBeccaEntity } from "../../server/src/becca/entities/abstract_becca_entity.js";
export type { default as BAttachment } from "../../server/src/becca/entities/battachment.js";
export type {
default as AbstractBeccaEntity
} from "../../server/src/becca/entities/abstract_becca_entity.js";
export type {
default as BAttachment
} from "../../server/src/becca/entities/battachment.js";
export type { default as BAttribute } from "../../server/src/becca/entities/battribute.js";
export type { default as BBranch } from "../../server/src/becca/entities/bbranch.js";
export type { default as BEtapiToken } from "../../server/src/becca/entities/betapi_token.js";
@ -31,6 +35,7 @@ export type { Api };
const fakeNote = new BNote();
/**
* The `api` global variable allows access to the backend script API, which is documented in {@link Api}.
* The `api` global variable allows access to the backend script API,
* which is documented in {@link Api}.
*/
export const api: Api = new BackendScriptApi(fakeNote, {});

View File

@ -1,19 +1,90 @@
process.env.TRILIUM_INTEGRATION_TEST = "memory-no-store";
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
// Only set TRILIUM_RESOURCE_DIR if not already set (e.g., by Nix wrapper)
if (!process.env.TRILIUM_RESOURCE_DIR) {
process.env.TRILIUM_RESOURCE_DIR = "../server/src";
}
process.env.NODE_ENV = "development";
import cls from "@triliumnext/server/src/services/cls.js";
import { dirname, join, resolve } from "path";
import archiver from "archiver";
import { execSync } from "child_process";
import { WriteStream } from "fs";
import * as fs from "fs/promises";
import * as fsExtra from "fs-extra";
import archiver from "archiver";
import { WriteStream } from "fs";
import { execSync } from "child_process";
import yaml from "js-yaml";
import { dirname, join, resolve } from "path";
import BuildContext from "./context.js";
interface NoteMapping {
rootNoteId: string;
path: string;
format: "markdown" | "html" | "share";
ignoredFiles?: string[];
exportOnly?: boolean;
}
interface Config {
baseUrl: string;
noteMappings: NoteMapping[];
}
const DOCS_ROOT = "../../../docs";
const OUTPUT_DIR = "../../site";
// Load configuration from edit-docs-config.yaml
async function loadConfig(configPath?: string): Promise<Config | null> {
const pathsToTry = configPath
? [resolve(configPath)]
: [
join(process.cwd(), "edit-docs-config.yaml"),
join(__dirname, "../../../edit-docs-config.yaml")
];
for (const path of pathsToTry) {
try {
const configContent = await fs.readFile(path, "utf-8");
const config = yaml.load(configContent) as Config;
// Resolve all paths relative to the config file's directory
const CONFIG_DIR = dirname(path);
config.noteMappings = config.noteMappings.map((mapping) => ({
...mapping,
path: resolve(CONFIG_DIR, mapping.path)
}));
return config;
} catch (error) {
if (error.code !== "ENOENT") {
throw error; // rethrow unexpected errors
}
}
}
return null; // No config file found
}
async function exportDocs(
noteId: string,
format: "markdown" | "html" | "share",
outputPath: string,
ignoredFiles?: string[]
) {
const zipFilePath = `output-${noteId}.zip`;
try {
const { exportToZipFile } = (await import("@triliumnext/server/src/services/export/zip.js"))
.default;
await exportToZipFile(noteId, format, zipFilePath, {});
const ignoredSet = ignoredFiles ? new Set(ignoredFiles) : undefined;
await extractZip(zipFilePath, outputPath, ignoredSet);
} finally {
if (await fsExtra.exists(zipFilePath)) {
await fsExtra.rm(zipFilePath);
}
}
}
async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
const note = await importData(sourcePath);
@ -21,15 +92,18 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
const zipName = outputSubDir || "user-guide";
const zipFilePath = `output-${zipName}.zip`;
try {
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js"))
.default;
const branch = note.getParentBranches()[0];
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
"no-progress-reporting",
"export",
null
);
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js"))
.default(
"no-progress-reporting",
"export",
null
);
const fileOutputStream = fsExtra.createWriteStream(zipFilePath);
await exportToZip(taskContext, branch, "share", fileOutputStream);
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
await waitForStreamToFinish(fileOutputStream);
// Output to root directory if outputSubDir is empty, otherwise to subdirectory
@ -42,7 +116,7 @@ async function importAndExportDocs(sourcePath: string, outputSubDir: string) {
}
}
async function buildDocsInner() {
async function buildDocsInner(config?: Config) {
const i18n = await import("@triliumnext/server/src/services/i18n.js");
await i18n.initializeTranslations();
@ -53,18 +127,49 @@ async function buildDocsInner() {
const beccaLoader = await import("../../server/src/becca/becca_loader.js");
await beccaLoader.beccaLoaded;
// Build User Guide
console.log("Building User Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
if (config) {
// Config-based build (reads from edit-docs-config.yaml)
console.log("Building documentation from config file...");
// Build Developer Guide
console.log("Building Developer Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "Developer Guide"), "developer-guide");
// Import all non-export-only mappings
for (const mapping of config.noteMappings) {
if (!mapping.exportOnly) {
console.log(`Importing from ${mapping.path}...`);
await importData(mapping.path);
}
}
// Copy favicon.
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "user-guide", "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico", join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
// Export all mappings
for (const mapping of config.noteMappings) {
if (mapping.exportOnly) {
console.log(`Exporting ${mapping.format} to ${mapping.path}...`);
await exportDocs(
mapping.rootNoteId,
mapping.format,
mapping.path,
mapping.ignoredFiles
);
}
}
} else {
// Legacy hardcoded build (for backward compatibility)
console.log("Building User Guide...");
await importAndExportDocs(join(__dirname, DOCS_ROOT, "User Guide"), "user-guide");
console.log("Building Developer Guide...");
await importAndExportDocs(
join(__dirname, DOCS_ROOT, "Developer Guide"),
"developer-guide"
);
// Copy favicon.
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
join(OUTPUT_DIR, "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
join(OUTPUT_DIR, "user-guide", "favicon.ico"));
await fs.copyFile("../../apps/website/src/assets/favicon.ico",
join(OUTPUT_DIR, "developer-guide", "favicon.ico"));
}
console.log("Documentation built successfully!");
}
@ -91,12 +196,13 @@ async function createImportZip(path: string) {
zlib: { level: 0 }
});
console.log("Archive path is ", resolve(path))
console.log("Archive path is ", resolve(path));
archive.directory(path, "/");
const outputStream = fsExtra.createWriteStream(inputFile);
archive.pipe(outputStream);
archive.finalize();
const { waitForStreamToFinish } = await import("@triliumnext/server/src/services/utils.js");
await waitForStreamToFinish(outputStream);
try {
@ -106,15 +212,15 @@ async function createImportZip(path: string) {
}
}
function waitForStreamToFinish(stream: WriteStream) {
return new Promise<void>((res, rej) => {
stream.on("finish", () => res());
stream.on("error", (err) => rej(err));
});
}
export async function extractZip(zipFilePath: string, outputPath: string, ignoredFiles?: Set<string>) {
const { readZipFile, readContent } = (await import("@triliumnext/server/src/services/import/zip.js"));
export async function extractZip(
zipFilePath: string,
outputPath: string,
ignoredFiles?: Set<string>
) {
const { readZipFile, readContent } = (await import(
"@triliumnext/server/src/services/import/zip.js"
));
await readZipFile(await fs.readFile(zipFilePath), async (zip, entry) => {
// We ignore directories since they can appear out of order anyway.
if (!entry.fileName.endsWith("/") && !ignoredFiles?.has(entry.fileName)) {
@ -129,6 +235,27 @@ export async function extractZip(zipFilePath: string, outputPath: string, ignore
});
}
export async function buildDocsFromConfig(configPath?: string, gitRootDir?: string) {
const config = await loadConfig(configPath);
if (gitRootDir) {
// Build the share theme if we have a gitRootDir (for Trilium project)
execSync(`pnpm run --filter share-theme build`, {
stdio: "inherit",
cwd: gitRootDir
});
}
// Trigger the actual build.
await new Promise((res, rej) => {
cls.init(() => {
buildDocsInner(config ?? undefined)
.catch(rej)
.then(res);
});
});
}
export default async function buildDocs({ gitRootDir }: BuildContext) {
// Build the share theme.
execSync(`pnpm run --filter share-theme build`, {

View File

@ -0,0 +1,89 @@
#!/usr/bin/env node
import packageJson from "../package.json" with { type: "json" };
import { buildDocsFromConfig } from "./build-docs.js";
// Parse command-line arguments
function parseArgs() {
const args = process.argv.slice(2);
let configPath: string | undefined;
let showHelp = false;
let showVersion = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === "--config" || args[i] === "-c") {
configPath = args[i + 1];
if (!configPath) {
console.error("Error: --config/-c requires a path argument");
process.exit(1);
}
i++; // Skip the next argument as it's the value
} else if (args[i] === "--help" || args[i] === "-h") {
showHelp = true;
} else if (args[i] === "--version" || args[i] === "-v") {
showVersion = true;
}
}
return { configPath, showHelp, showVersion };
}
function getVersion(): string {
return packageJson.version;
}
function printHelp() {
const version = getVersion();
console.log(`
Usage: trilium-build-docs [options]
Options:
-c, --config <path> Path to the configuration file
(default: edit-docs-config.yaml in current directory)
-h, --help Display this help message
-v, --version Display version information
Description:
Builds documentation from Trilium note structure and exports to various formats.
Configuration file should be in YAML format with the following structure:
baseUrl: "https://example.com"
noteMappings:
- rootNoteId: "noteId123"
path: "docs"
format: "markdown"
- rootNoteId: "noteId456"
path: "public/docs"
format: "share"
exportOnly: true
Version: ${version}
`);
}
function printVersion() {
const version = getVersion();
console.log(version);
}
async function main() {
const { configPath, showHelp, showVersion } = parseArgs();
if (showHelp) {
printHelp();
process.exit(0);
} else if (showVersion) {
printVersion();
process.exit(0);
}
try {
await buildDocsFromConfig(configPath);
process.exit(0);
} catch (error) {
console.error("Error building documentation:", error);
process.exit(1);
}
}
main();

View File

@ -13,16 +13,19 @@
* Make sure to keep in line with frontend's `script_context.ts`.
*/
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
export type { default as FAttachment } from "../../client/src/entities/fattachment.js";
export type { default as FAttribute } from "../../client/src/entities/fattribute.js";
export type { default as FBranch } from "../../client/src/entities/fbranch.js";
export type { default as FNote } from "../../client/src/entities/fnote.js";
export type { Api } from "../../client/src/services/frontend_script_api.js";
export type { default as NoteContextAwareWidget } from "../../client/src/widgets/note_context_aware_widget.js";
export type { default as BasicWidget } from "../../client/src/widgets/basic_widget.js";
export type {
default as NoteContextAwareWidget
} from "../../client/src/widgets/note_context_aware_widget.js";
export type { default as RightPanelWidget } from "../../client/src/widgets/right_panel_widget.js";
import FrontendScriptApi, { type Api } from "../../client/src/services/frontend_script_api.js";
//@ts-expect-error
// @ts-expect-error - FrontendScriptApi is not directly exportable as Api without this simulation.
export const api: Api = new FrontendScriptApi();

View File

@ -1,9 +1,10 @@
import { join } from "path";
import BuildContext from "./context";
import buildSwagger from "./swagger";
import { cpSync, existsSync, mkdirSync, rmSync } from "fs";
import { join } from "path";
import buildDocs from "./build-docs";
import BuildContext from "./context";
import buildScriptApi from "./script-api";
import buildSwagger from "./swagger";
const context: BuildContext = {
gitRootDir: join(__dirname, "../../../"),

View File

@ -1,7 +1,8 @@
import { execSync } from "child_process";
import BuildContext from "./context";
import { join } from "path";
import BuildContext from "./context";
export default function buildScriptApi({ baseDir, gitRootDir }: BuildContext) {
// Generate types
execSync(`pnpm typecheck`, { stdio: "inherit", cwd: gitRootDir });

View File

@ -1,7 +1,8 @@
import BuildContext from "./context";
import { join } from "path";
import { execSync } from "child_process";
import { mkdirSync } from "fs";
import { join } from "path";
import BuildContext from "./context";
interface BuildInfo {
specPath: string;
@ -27,6 +28,9 @@ export default function buildSwagger({ baseDir, gitRootDir }: BuildContext) {
const absSpecPath = join(gitRootDir, specPath);
const targetDir = join(baseDir, outDir);
mkdirSync(targetDir, { recursive: true });
execSync(`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`, { stdio: "inherit" });
execSync(
`pnpm redocly build-docs ${absSpecPath} -o ${targetDir}/index.html`,
{ stdio: "inherit" }
);
}
}

View File

@ -1,6 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"include": [],
"include": [
"scripts/**/*.ts"
],
"references": [
{
"path": "../server"

View File

@ -4,6 +4,7 @@
"entryPoints": [
"src/backend_script_entrypoint.ts"
],
"tsconfig": "tsconfig.app.json",
"plugin": [
"typedoc-plugin-missing-exports"
]

View File

@ -4,6 +4,7 @@
"entryPoints": [
"src/frontend_script_entrypoint.ts"
],
"tsconfig": "tsconfig.app.json",
"plugin": [
"typedoc-plugin-missing-exports"
]

View File

@ -46,10 +46,6 @@ if (utils.isElectron()) {
electronContextMenu.setupContextMenu();
}
if (utils.isPWA()) {
initPWATopbarColor();
}
function initOnElectron() {
const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
@ -134,20 +130,3 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
nativeTheme.themeSource = themeSource;
}
function initPWATopbarColor() {
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}

View File

@ -1,3 +1,7 @@
#background-color-tracker {
color: var(--main-background-color) !important;
}
span.keyboard-shortcut,
kbd {
display: none;

View File

@ -77,7 +77,6 @@ export default class MobileLayout {
.child(<NoteTitleActions />)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
)

View File

@ -1,12 +1,12 @@
import treeService from "../services/tree.js";
import froca from "../services/froca.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from "../services/i18n.js";
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import treeService from "../services/tree.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const parentNoteId = this.node.getParent().data.noteId;
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem;

View File

@ -410,6 +410,7 @@ body.desktop .tabulator-popup-container,
.dropdown-menu.static {
box-shadow: unset;
backdrop-filter: unset !important;
}
.dropend .dropdown-toggle::after {
@ -1553,8 +1554,14 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
}
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
body.mobile .dropdown-menu.mobile-bottom-menu.show {
--dropdown-bottom: 0px;
padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important;
}
#mobile-sidebar-container {

View File

@ -57,12 +57,12 @@
height: 18px;
}
/*
/*
* SEARCH PAGE
*/
/* Button bar */
.search-definition-widget .search-setting-table tbody:last-child div {
.search-definition-widget .search-setting-table .search-actions-container {
justify-content: flex-end;
gap: 8px;
}
@ -143,7 +143,7 @@
/*
* OPTIONS PAGES
*/
:root {
--options-card-min-width: 500px;
--options-card-max-width: 900px;
@ -335,4 +335,4 @@ nav.options-section-tabs + .options-section {
.etapi-options-section div {
height: auto !important;
}
}

View File

@ -662,7 +662,8 @@
"show-cheatsheet": "Show Cheatsheet",
"toggle-zen-mode": "Zen Mode",
"new-version-available": "New Update Available",
"download-update": "Get Version {{latestVersion}}"
"download-update": "Get Version {{latestVersion}}",
"search_notes": "Search notes"
},
"zen_mode": {
"button_exit": "Exit Zen Mode"
@ -761,7 +762,8 @@
"note_revisions": "Note revisions",
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
"error_unrecognized_command": "Unrecognized command {{command}}",
"backlinks": "Backlinks"
"backlinks": "Backlinks",
"content_language_switcher": "Content language: {{language}}"
},
"note_icon": {
"change_note_icon": "Change note icon",
@ -906,6 +908,7 @@
"debug": "debug",
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
"action": "action",
"option": "option",
"search_button": "Search",
"search_execute": "Search & Execute actions",
"save_to_note": "Save to note",
@ -2277,5 +2280,8 @@
"title_one": "{{count}} tab",
"title_other": "{{count}} tabs",
"more_options": "More options"
},
"bookmark_buttons": {
"bookmarks": "Bookmarks"
}
}

View File

@ -12,6 +12,7 @@
.fixed-note-tree-container {
height: 60%;
border-bottom: 1px solid var(--main-border-color);
overflow: auto;
.tree-wrapper {
padding: 0;

View File

@ -38,7 +38,7 @@ export default function NoteDetail() {
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
const widgetRequestId = useRef(0);
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile();
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
const props: TypeWidgetProps = {
note: note!,
@ -216,7 +216,7 @@ export default function NoteDetail() {
"fixed-tree": hasFixedTree
})}
>
{hasFixedTree && <FixedTree />}
{hasFixedTree && <FixedTree noteContext={noteContext} />}
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper
@ -232,8 +232,7 @@ export default function NoteDetail() {
);
}
function FixedTree() {
const { noteContext } = useNoteContext();
function FixedTree({ noteContext }: { noteContext: NoteContext }) {
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
return <div class="fixed-note-tree-container">{treeEl}</div>;
}

View File

@ -45,6 +45,10 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
noDropdownListStyle
mobileBackdrop
>
{isMobile() && <>
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
<FormDropdownDivider />
</>}
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
@ -105,7 +109,7 @@ function BrowserOnlyOptions() {
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
return <>
<FormListHeader text="Development Options"></FormListHeader>
<FormListHeader text="Development Options" />
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
{experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />

View File

@ -10,12 +10,6 @@
--card-padding: 0.6em;
}
body.mobile .board-view {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container {
height: 100%;
display: flex;
@ -26,6 +20,12 @@ body.mobile .board-view {
overflow-x: auto;
}
body.mobile .board-view-container {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container .board-column {
width: 250px;
flex-shrink: 0;

View File

@ -18,14 +18,12 @@ import FlexContainer from "./flex_container.js";
* - `#root-container.vertical-layout`, if the current layout is horizontal.
*/
export default class RootContainer extends FlexContainer<BasicWidget> {
private originalViewportHeight: number;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.id("root-widget");
this.css("height", "100dvh");
this.originalViewportHeight = getViewportHeight();
}
render(): JQuery<HTMLElement> {
@ -40,6 +38,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
this.#initPWATopbarColor();
return super.render();
}
@ -65,8 +64,12 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
#onMobileResize() {
const currentViewportHeight = getViewportHeight();
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const windowHeight = window.innerHeight;
// If viewport is significantly smaller, keyboard is likely open
const isKeyboardOpened = windowHeight - viewportHeight > 150;
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
@ -113,8 +116,23 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
document.body.lang = locale;
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}
#initPWATopbarColor() {
if (!utils.isPWA()) return;
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}
}
function getViewportHeight() {
return window.visualViewport?.height ?? window.innerHeight;
}

View File

@ -1,11 +1,16 @@
import { useContext, useMemo } from "preact/hooks";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
import "./BookmarkButtons.css";
import { CSSProperties } from "preact";
import { useContext, useMemo } from "preact/hooks";
import type FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks";
import NoteLink from "../react/NoteLink";
import { CustomNoteLauncher } from "./GenericButtons";
import ResponsiveContainer from "../react/ResponsiveContainer";
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
const PARENT_NOTE_ID = "_lbBookmarks";
@ -19,17 +24,64 @@ export default function BookmarkButtons() {
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
)
<ResponsiveContainer
desktop={
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
}
mobile={
<LaunchBarDropdownButton
icon="bx bx-bookmark"
title={t("bookmark_buttons.bookmarks")}
>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</LaunchBarDropdownButton>
}
/>
);
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
return <ResponsiveContainer
desktop={
bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
mobile={<MobileBookmarkItem noteId={note.noteId} bookmarkFolder={bookmarkFolder} />}
/>;
}
function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) {
const note = useNote(noteId);
const noteIcon = useNoteIcon(note);
if (!note) return null;
return (
!bookmarkFolder
? <FormListItem icon={noteIcon} onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title}</FormListItem>
: <MobileBookmarkFolder note={note} />
);
}
function MobileBookmarkFolder({ note }: { note: FNote }) {
const childNotes = useChildNotes(note.noteId);
return (
<FormDropdownSubmenu icon="bx bx-folder" title={note.title}>
{childNotes.map(childNote => (
<FormListItem
key={childNote.noteId}
icon={childNote.getIcon()}
onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })}
>
{childNote.title}
</FormListItem>
))}
</FormDropdownSubmenu>
);
}
function BookmarkFolder({ note }: { note: FNote }) {
@ -55,5 +107,5 @@ function BookmarkFolder({ note }: { note: FNote }) {
</ul>
</div>
</LaunchBarDropdownButton>
)
);
}

View File

@ -7,32 +7,18 @@ import { isCtrlKey } from "../../services/utils";
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
export function CustomNoteLauncher(props: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
keyboardShortcut?: string;
}) {
const { launcherNote, getTargetNoteId } = props;
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
if (evt.which === 3) {
return;
}
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
await launchCustomNoteLauncher(evt, props);
}, [ props ]);
// Keyboard shortcut.
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
@ -54,3 +40,24 @@ export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNo
/>
);
}
export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
}) {
if (evt.which === 3) return;
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}

View File

@ -49,6 +49,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
placement: isHorizontalLayout ? "bottom" : "right"
}
}}
mobileBackdrop
{...props}
>{children}</Dropdown>
);

View File

@ -50,6 +50,9 @@ body.experimental-feature-new-layout {
}
}
}
body.desktop .title-actions {
> .collapsible,
> .note-type-switcher {
padding-inline-start: calc(24px - var(--title-actions-padding-start));
@ -57,3 +60,4 @@ body.experimental-feature-new-layout {
}
}
}

View File

@ -23,7 +23,7 @@ export default function NoteTitleActions() {
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
<EditedNotes />
{viewScope?.viewMode === "default" && <NoteTypeSwitcher />}
{(!viewScope?.viewMode || viewScope.viewMode === "default") && <NoteTypeSwitcher />}
</div>
);
}

View File

@ -58,33 +58,6 @@
.dropdown-note-info {
padding: 1em !important;
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.dropdown-note-paths {
@ -144,16 +117,50 @@
}
div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
button.select-button:not(:focus-visible) {
outline: none;
}
}
body.experimental-feature-new-layout .note-info-content {
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
padding: 0;
}
.bottom-panel {
margin: 0 !important;
padding: 0;

View File

@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) {
//#region Note info & Similar
interface NoteInfoContext extends StatusBarContext {
similarNotesShown: boolean;
setSimilarNotesShown: (value: boolean) => void;
similarNotesShown?: boolean;
setSimilarNotesShown?: (value: boolean) => void;
}
export function NoteInfoBadge(context: NoteInfoContext) {
@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) {
// Keyboard shortcut.
useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show());
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown));
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown));
return (enabled &&
<StatusBarDropdown
@ -242,8 +242,8 @@ export function NoteInfoBadge(context: NoteInfoContext) {
);
}
function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: NoteInfoContext & {
dropdownRef: RefObject<BootstrapDropdown>;
export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick<NoteInfoContext, "note" | "setSimilarNotesShown"> & {
dropdownRef?: RefObject<BootstrapDropdown>;
noteType: NoteType;
}) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
@ -251,7 +251,7 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]);
return (
<>
<div className="note-info-content">
<ul>
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
@ -262,14 +262,14 @@ function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }:
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul>
<LinkButton
{setSimilarNotesShown && <LinkButton
text={t("note_info_widget.show_similar_notes")}
onClick={() => {
dropdownRef.current?.hide();
dropdownRef?.current?.hide();
setSimilarNotesShown(true);
}}
/>
</>
/>}
</div>
);
}

View File

@ -0,0 +1,3 @@
.code-note-switcher-modal .dropdown-menu {
background: none !important;
}

View File

@ -1,17 +1,24 @@
import "./mobile_detail_menu.css";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import { createPortal, useRef, useState } from "preact/compat";
import FNote, { NotePathRecord } from "../../entities/fnote";
import { t } from "../../services/i18n";
import note_create from "../../services/note_create";
import server from "../../services/server";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import { getLocaleName, NoteInfoContent } from "../layout/StatusBar";
import ActionButton from "../react/ActionButton";
import { FormDropdownDivider, FormListItem } from "../react/FormList";
import { useNoteContext } from "../react/hooks";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import Modal from "../react/Modal";
import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import { NoteContextMenu } from "../ribbon/NoteActions";
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
export default function MobileDetailMenu() {
const dropdownRef = useRef<BootstrapDropdown | null>(null);
@ -20,6 +27,9 @@ export default function MobileDetailMenu() {
const isMainContext = noteContext?.isMainContext();
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false);
const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false);
const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false);
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
@ -36,7 +46,7 @@ export default function MobileDetailMenu() {
<NoteContextMenu
dropdownRef={dropdownRef}
note={note} noteContext={noteContext}
extraItems={<>
itemsAtStart={<>
<div className="form-list-row">
<div className="form-list-col">
<FormListItem
@ -81,6 +91,13 @@ export default function MobileDetailMenu() {
</>}
<FormDropdownDivider />
</>}
itemsNearNoteSettings={<>
{note.type === "text" && <ContentLanguageSelector note={note} />}
{note.type === "code" && <FormListItem icon={"bx bx-code"} onClick={() => setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}</FormListItem>}
<FormListItem icon="bx bx-info-circle" onClick={() => setNoteInfoModalShown(true)}>{t("note_info_widget.title")}</FormListItem>
<FormListItem icon="bx bx-bar-chart" onClick={() => setSimilarNotesModalShown(true)}>{t("similar_notes.title")}</FormListItem>
<FormDropdownDivider />
</>}
/>
) : (
<ActionButton
@ -94,13 +111,46 @@ export default function MobileDetailMenu() {
<>
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
<NoteInfoModal note={note} modalShown={noteInfoModalShown} setModalShown={setNoteInfoModalShown} />
<SimilarNotesModal note={note} modalShown={similarNotesModalShown} setModalShown={setSimilarNotesModalShown} />
<CodeNoteSwitcherModal note={note} modalShown={codeNoteSwitcherModalShown} setModalShown={setCodeNoteSwitcherModalShown} />
</>
), document.body)}
</div>
);
}
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, setModalShown: (shown: boolean) => void }) {
function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) {
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
return (
<FormDropdownSubmenu
icon="bx bx-globe"
title={t("mobile_detail_menu.content_language_switcher", { language: getLocaleName(activeLocale ?? DEFAULT_LOCALE) })}
>
{processedLocales.map((locale, index) =>
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentNoteLanguage}
onClick={() => setCurrentNoteLanguage(locale.id)}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
)}
</FormDropdownSubmenu>
);
}
interface WithModal {
modalShown: boolean;
setModalShown: (shown: boolean) => void;
}
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="backlinks-modal tn-backlinks-widget"
@ -116,7 +166,7 @@ function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | nul
);
}
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, modalShown: boolean, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined, setModalShown: (shown: boolean) => void }) {
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) {
return (
<Modal
className="note-paths-modal"
@ -134,3 +184,57 @@ function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalS
</Modal>
);
}
function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="note-info-modal"
size="md"
title={t("note_info_widget.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
{note && <NoteInfoContent note={note} noteType={note.type} />}
</Modal>
);
}
function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="similar-notes-modal"
size="md"
title={t("similar_notes.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<SimilarNotesTab note={note} />
</Modal>
);
}
function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
const currentNoteMime = useNoteProperty(note, "mime");
const mimeTypes = useMimeTypes();
return (
<Modal
className="code-note-switcher-modal"
size="md"
title={t("status_bar.code_note_switcher")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<div className="dropdown-menu static show">
{note && <NoteTypeCodeNoteList
currentMimeType={currentNoteMime}
mimeTypes={mimeTypes}
changeNoteType={(type, mime) => {
server.put(`notes/${note.noteId}/type`, { type, mime });
setModalShown(false);
}}
/>}
</div>
</Modal>
);
}

View File

@ -0,0 +1,12 @@
import { ComponentChildren } from "preact";
import { isMobile } from "../../services/utils";
interface ResponsiveContainerProps {
mobile?: ComponentChildren;
desktop?: ComponentChildren;
}
export default function ResponsiveContainer({ mobile, desktop }: ResponsiveContainerProps) {
return (isMobile() ? mobile : desktop);
}

View File

@ -1,5 +1,4 @@
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
@ -117,19 +116,18 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
}
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
})}
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
@ -141,7 +139,7 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
currentMimeType?: string;
mimeTypes: MimeType[];
changeNoteType(type: NoteType, mime: string): void;
setModalShown(shown: boolean): void;
setModalShown?(shown: boolean): void;
}) {
return (
<>
@ -155,8 +153,10 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
</FormListItem>
))}
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
{setModalShown && <>
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</>}
</>
);
}
@ -195,7 +195,7 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
</div>
)
);
}
function EditabilitySelect({ note }: { note?: FNote | null }) {
@ -417,9 +417,9 @@ function findTypeTitle(type?: NoteType, mime?: string | null) {
const found = mimeTypes.find((mt) => mt.mime === mime);
return found ? found.title : mime;
} else {
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}

View File

@ -63,10 +63,11 @@ function RevisionsButton({ note }: { note: FNote }) {
type ItemToFocus = "basic-properties";
export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: externalDropdownRef }: {
export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNoteSettings, dropdownRef: externalDropdownRef }: {
note: FNote,
noteContext?: NoteContext,
extraItems?: ComponentChildren;
itemsAtStart?: ComponentChildren;
itemsNearNoteSettings?: ComponentChildren;
dropdownRef?: RefObject<BootstrapDropdown>;
}) {
const dropdownRef = useSyncedRef<BootstrapDropdown>(externalDropdownRef, null);
@ -112,7 +113,7 @@ export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: ex
onHidden={() => itemToFocusRef.current = null }
mobileBackdrop
>
{extraItems}
{itemsAtStart}
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
@ -131,6 +132,8 @@ export function NoteContextMenu({ note, noteContext, extraItems, dropdownRef: ex
<FormDropdownDivider />
</>}
{itemsNearNoteSettings}
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />

View File

@ -2,6 +2,7 @@ import "./SearchDefinitionTab.css";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
@ -15,12 +16,13 @@ import tree from "../../services/tree";
import { getErrorMessage } from "../../services/utils";
import ws from "../../services/ws";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import Button from "../react/Button";
import Button, { SplitButton } from "../react/Button";
import Dropdown from "../react/Dropdown";
import { FormListHeader, FormListItem } from "../react/FormList";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import ResponsiveContainer from "../react/ResponsiveContainer";
import { TabContext } from "./ribbon-interface";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
@ -84,15 +86,32 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
<tr>
<td className="title-column">{t("search_definition.add_search_option")}</td>
<td colSpan={2} className="add-search-option">
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
size="small"
icon={icon}
text={label}
title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
<ResponsiveContainer
desktop={searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
key={`${attributeType}-${attributeName}`}
size="small" icon={icon} text={label} title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
mobile={
<Dropdown
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bx-plus" />{" "}{t("search_definition.option")}</>}
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
noSelectButtonStyle
>
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<FormListItem
key={`${attributeType}-${attributeName}`}
icon={icon}
description={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
>{label}</FormListItem>
))}
</Dropdown>
}
/>
<AddBulkActionButton note={note} />
</td>
@ -113,48 +132,7 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
})}
</tbody>
<BulkActionsList note={note} />
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<div className="search-actions-container">
<Button
icon="bx bx-search"
text={t("search_definition.search_button")}
keyboardShortcut="Enter"
onClick={refreshResults}
/>
<Button
icon="bx bxs-zap"
text={t("search_definition.search_execute")}
onClick={async () => {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}}
/>
{note.isHiddenCompletely() && <Button
icon="bx bx-save"
text={t("search_definition.save_to_note")}
onClick={async () => {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) {
return;
}
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}}
/>}
</div>
</td>
</tr>
</tbody>
<SearchButtonBar note={note} refreshResults={refreshResults} />
</table>
)}
</div>
@ -162,6 +140,56 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
);
}
function SearchButtonBar({ note, refreshResults }: {
note: FNote;
refreshResults(): void;
}) {
async function searchAndExecuteActions() {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}
async function saveSearchNote() {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) return;
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}
return (
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<ResponsiveContainer
desktop={
<div className="search-actions-container">
<Button icon="bx bx-search" text={t("search_definition.search_button")} keyboardShortcut="Enter" onClick={refreshResults} />
<Button icon="bx bxs-zap" text={t("search_definition.search_execute")} onClick={searchAndExecuteActions} />
{note.isHiddenCompletely() && <Button icon="bx bx-save" text={t("search_definition.save_to_note")} onClick={saveSearchNote} />}
</div>
}
mobile={
<SplitButton
text={t("search_definition.search_button")} icon="bx bx-search"
onClick={refreshResults}
>
<FormListItem icon="bx bxs-zap" onClick={searchAndExecuteActions}>{t("search_definition.search_execute")}</FormListItem>
{note.isHiddenCompletely() && <FormListItem icon="bx bx-save" onClick={saveSearchNote}>{t("search_definition.save_to_note")}</FormListItem>}
</SplitButton>
}
/>
</td>
</tr>
</tbody>
);
}
function BulkActionsList({ note }: { note: FNote }) {
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
@ -194,15 +222,18 @@ function AddBulkActionButton({ note }: { note: FNote }) {
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
noSelectButtonStyle
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
>
{ACTION_GROUPS.map(({ actions, title }) => (
<>
{ACTION_GROUPS.map(({ actions, title }, index) => (
<Fragment key={index}>
<FormListHeader text={title} />
{actions.map(({ actionName, actionTitle }) => (
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</>
<div>
{actions.map(({ actionName, actionTitle }) => (
<FormListItem key={actionName} onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</div>
</Fragment>
))}
</Dropdown>
);

View File

@ -10,6 +10,12 @@
display: flex;
justify-content: space-between;
align-items: baseline;
@media (max-width: 991px) {
margin-block: 1em;
flex-direction: column;
gap: 0.5em;
}
}
/* #endregion */
@ -42,6 +48,12 @@
display: flex;
align-items: center;
gap: 1em;
@media (max-width: 991px) {
gap: 0.5em;
flex-wrap: wrap;
.attachment-title { flex-grow: 1; }
}
}
.attachment-details {
@ -127,10 +139,6 @@
/* #region Attachment actions */
.attachment-actions .dropdown-menu {
width: 20em;
}
.attachment-actions .dropdown-item .tn-icon {
position: relative;
top: 3px;

View File

@ -239,6 +239,8 @@ function AttachmentActions({ attachment, copyAttachmentLinkToClipboard }: { atta
text={<Icon icon="bx bx-dots-vertical-rounded" />}
buttonClassName="icon-action-always-border"
iconAction
dropdownContainerClassName="mobile-bottom-menu"
mobileBackdrop
>
<FormListItem
icon="bx bx-file-find"

View File

@ -1,12 +1,18 @@
<p>To easily access selected notes, you can bookmark them. See demo:</p>
<p>
<img src="Bookmarks_bookmarks.gif">
</p>
<p>Frequently used notes can be bookmarked, which will make them appear in
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;for
easy access.</p>
<h2>Configuring the launch bar</h2>
<p>If bookmarks don't appear in the launch bar, then most likely the bookmark
section has been hidden. Go to the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;configuration
from the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
ensure <em>Bookmarks</em> is in the <em>Visible Launchers</em> section.</p>
<h2>Bookmark folder</h2>
<p>Space in the left panel is limited, and you might want to bookmark many
items. One possible solution is to bookmark a folder, so it shows its children:</p>
<p>
<img src="Bookmarks_bookmark-folder.png">
</p>
<p>To do this, you need to add a <code spellcheck="false">#bookmarkFolder</code> label
to the note.</p>
<p>To do this, bookmark a folder and assign it the <code spellcheck="false">#bookmarkFolder</code> label.</p>
<h2>Mobile</h2>
<p>On mobile, bookmarks are only displayed starting with v0.102.0. Because
of the more constrained screen size, the bookmarks are grouped under a
single icon instead of displaying them as separate icons.</p>
<p>When pressed, a menu will appear listing all the bookmarks. Bookmark folders
are also supported and will appear as sub-menus.</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

View File

@ -1,8 +1,11 @@
<h2>Position of the Launch bar</h2>
<p>Depending on the layout selected, the launcher bar will either be on the
left side of the screen with buttons displayed vertically or at the top
of the screen. See&nbsp;<a href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>&nbsp;for
<p>On desktop, depending on the layout selected, the launcher bar will either
be on the left side of the screen with buttons displayed vertically or
at the top of the screen. See&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>&nbsp;for
more information.</p>
<p>On mobile, the launch bar will always be at the bottom.</p>
<p>If there are too many items in the launch bar to fit the screen, it will
become scrollable.</p>
<h2>Terminology</h2>
<ul>
<li><strong>Launcher</strong>: a button that can be (or is) displayed on the
@ -12,16 +15,16 @@
<li><strong>Visible Launcher</strong>: a launcher that is currently displayed
on the launch bar.</li>
</ul>
<h2>Configuring the Launch bar</h2>
<h2>Configuring the desktop Launch bar</h2>
<p>There are two ways to configure the launch bar:</p>
<ul>
<li>Right click in the empty space between launchers on the launch bar and
select <em>Configure Launchbar.</em>
</li>
<li>Click on the&nbsp;<a href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
<li>Click on the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
select <em>Configure Launchbar</em>.</li>
</ul>
<p>This will open a new tab with the&nbsp;<a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;listing
<p>This will open a new tab with the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;listing
the launchers.</p>
<p>
<img src="Launch Bar_image.png">
@ -29,6 +32,19 @@
<p>Expanding <em>Available Launchers</em> section will show the list of launchers
that are not displayed on the launch bar. The <em>Visible Launchers</em> will
show the ones that are currently displayed.</p>
<h2>Configuring the mobile launch bar</h2>
<p>The launch bar on mobile uses a different configuration from the desktop
one. The reasoning is that not all desktop icons are available on mobile,
and fewer icons fit on a mobile screen.</p>
<p>To configure the launch bar on mobile, go to&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
select <em>Configure Launchbar</em>.</p>
<p>The configure the mobile launch bar while on the desktop (especially useful
to configure more complicated launchers such as scripts or custom widgets),
go to&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>&nbsp;
Advanced → Show Hidden Subtree and look for the <em>Mobile Launch Bar</em> section.
While in the hidden subtree, it's also possible to drag launchers between
the <em>Mobile Launch Bar</em> and (Desktop) <em>Launch Bar</em> sections.</p>
<h3>Adding/removing and reordering launchers</h3>
<p>To display a new launcher in the launch bar, first look for it in the <em>Available Launchers</em> section.
Then right click it and select <em>Move to visible launchers</em>. It is
@ -36,13 +52,13 @@
<p>Similarly, to remove it from the launch bar, simply look for it in <em>Visible Launchers</em> then
right click it and select <em>Move to available launchers</em> or use drag-and-drop.</p>
<p>Drag-and-drop the items in the&nbsp;tree&nbsp;in order to change their
order. See&nbsp;<a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;for more
interaction options, including using keyboard shortcuts.</p>
order. See&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;for
more interaction options, including using keyboard shortcuts.</p>
<h2>Customizing the launcher</h2>
<ul>
<li>The icon of a launcher can be changed just like a normal note. See&nbsp;
<a
href="#root/_help_p9kXRFAkwN4o">Note Icons</a>&nbsp;for more information.</li>
class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/BFs8mudNFgCS/_help_p9kXRFAkwN4o">Note Icons</a>&nbsp;for more information.</li>
<li>The title of the launcher can also be changed.</li>
</ul>
<h3>Resetting</h3>
@ -54,42 +70,39 @@
<p>Right click either the <em>Available launchers</em> or <em>Visible launchers</em> sections
and select one of the options:</p>
<ol>
<li>
<p><strong>Note Launcher</strong>
<br>A note launcher will simply navigate to a specified note.</p>
<li><strong>Note Launcher</strong>
<br>A note launcher will simply navigate to a specified note.
<ol>
<li>Set the <code spellcheck="false">target</code> promoted attribute to the
note to navigate to.</li>
<li>Optionally, set <code spellcheck="false">hoistedNote</code> to hoist a particular
note. See&nbsp;<a href="#root/_help_OR8WJ7Iz9K4U">Note Hoisting</a>&nbsp;for
note. See&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_OR8WJ7Iz9K4U">Note Hoisting</a>&nbsp;for
more information.</li>
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
the launcher.</li>
</ol>
</li>
<li>
<p><strong>Script Launcher</strong>
<br>An advanced launcher which will run a script upon pressing. See&nbsp;
<a
href="#root/_help_CdNpE2pqjmI6">Scripts</a>&nbsp;for more information.</p>
<ol>
<li>Set <code spellcheck="false">script</code> to point to the desired script
to run.</li>
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
the launcher.</li>
</ol>
<li><strong>Script Launcher</strong>
<br>An advanced launcher which will run a script upon pressing. See&nbsp;
<a
class="reference-link" href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">Scripting</a>&nbsp;for more information.
<ol>
<li>Set <code spellcheck="false">script</code> to point to the desired script
to run.</li>
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
the launcher.</li>
</ol>
</li>
<li>
<li class="ck-list-marker-bold">
<p><strong>Custom Widget</strong>
</p>
<p>Allows defining a custom widget to be rendered inside the launcher. See&nbsp;
<a
href="#root/_help_SynTBQiBsdYJ">Widget Basics</a>&nbsp;for more information.</p>
</li>
<li>
<p><strong>Spacers</strong>
<br>Launchers that create some distance between other launchers for better
visual distinction.</p>
class="reference-link" href="#root/pOsGYCXsbNQG/CdNpE2pqjmI6/yIhgI5H7A2Sm/MgibgPcfeuGz/_help_SynTBQiBsdYJ">Widget Basics</a>&nbsp;for more information.</p>
</li>
<li><strong>Spacers</strong>
<br>Launchers that create some distance between other launchers for better
visual distinction.</li>
</ol>
<p>Launchers are configured via predefined&nbsp;<a href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
<p>Launchers are configured via predefined&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/tC7s2alapj8V/zEY4DaJG4YT5/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>

View File

@ -4,8 +4,7 @@ class="image image-style-align-center">
<img style="aspect-ratio:1398/1015;" src="Split View_2_Split View_im.png"
width="1398" height="1015">
</figure>
<h2><strong>Interactions</strong></h2>
<h2><strong>Interactions</strong></h2>
<ul>
<li>Press the
<img src="Split View_Split View_imag.png">button to the right of a note's title to open a new split to the right
@ -51,11 +50,12 @@ class="image image-style-align-center">
<ul>
<li>On smartphones, the split views are laid out vertically (one on the top
and one on the bottom), instead of horizontally as on the desktop.</li>
<li>There can be only one split open per tab.</li>
<li>It's not possible to resize the two split panes.</li>
<li>When the keyboard is opened, the active note will be “maximized”, thus
allowing for more space even when a split is open. When the keyboard is
closed, the splits become equal in size again.</li>
<li
>There can be only one split open per tab.</li>
<li>It's not possible to resize the two split panes.</li>
<li>When the keyboard is opened, the active note will be “maximized”, thus
allowing for more space even when a split is open. When the keyboard is
closed, the splits become equal in size again.</li>
</ul>
<p>Interaction:</p>
<ul>

View File

@ -8,7 +8,8 @@
<ul>
<li>For the vertical layout, the tabs will be placed at the top but to the
right of the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>.</li>
<li>For the horizontal layout, the tabs will be placed at the top in full-width,
<li
>For the horizontal layout, the tabs will be placed at the top in full-width,
above the&nbsp;<a href="#root/_help_oPVyFC7WL2Lp">note tree</a>, allowing for
more tabs to be comfortably displayed.</li>
</ul>
@ -22,7 +23,8 @@
href="#root/_help_luNhaphA37EO">Split View</a>. Each tab can have one or more
notes, displayed horizontally.</li>
<li>Tabs can be reordered by drag-and-dropping it into a new position.</li>
<li>An existing tab can be displayed in a new window by dragging the tab upwards
<li
>An existing tab can be displayed in a new window by dragging the tab upwards
or downwards. It is not possible to combine tabs back into another window.</li>
</ul>
<h2>Keyboard interaction</h2>
@ -36,7 +38,29 @@
<li><kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> to
go to the next or previous tab.</li>
<li><kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to
activate the first, second and up til ninth tab.</li>
activate the first, second and up to ninth tab.</li>
<li>There is also a shortcut to go to the last tab, but it is not assigned
a key by default.</li>
</ul>
</ul>
<h2>Mobile</h2>
<figure class="image image-style-align-right image_resized" style="width:34.12%;">
<img style="aspect-ratio:1242/2688;" src="Tabs_IMG_1767.PNG"
width="1242" height="2688">
</figure>
<p>Tabs are also supported on the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/Otzi9La2YAUX/_help_RDslemsQ6gCp">Mobile Frontend</a>.</p>
<p>Since v0.102.0, the tabs are displayed by pressing the dedicated tab switcher
button in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
In this view the tabs are laid out on a grid with a preview of the note
content.</p>
<p>The context menu button at the top-right of the popup allows creating
a new tab, reopening the last closed tab and closing all the tabs.</p>
<p><a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_luNhaphA37EO">Split View</a>s
are also indicated in the tab switcher, with two titles displayed in a
tab.</p>
<aside class="admonition note">
<p>Versions prior to v0.102.0 also supported tabs, but they were displayed
directly above the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>.
The decision to use a more mobile-like tab switcher was taken because the
original tab bar could not support many tabs at once and the new design
better aligns with how mobile applications handle tabs.</p>
</aside>

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -1,35 +1,168 @@
<p>Trilium (<a href="#root/_help_WOcw2SLH6tbX">server edition</a>) has a mobile
web frontend which is optimized for touch based devices - smartphones and
tablets. It is activated automatically during login process based on browser
detection.</p>
<p>Mobile frontend is limited in features compared to full desktop frontend.
<figure class="image image_resized image-style-align-right" style="width:33.52%;">
<img style="aspect-ratio:1242/2688;" src="Mobile Frontend_IMG_1765.PNG"
width="1242" height="2688">
</figure>
<p>Trilium has a mobile web frontend which is optimized for touch based devices
- smartphones and tablets. It is activated automatically during login process
based on browser detection.</p>
<p>Mobile frontend is limited in features compared to the full desktop version.
See below for more details on this.</p>
<p>Note that this is not an Android/iOS app, this is just mobile friendly
web page served on the <a href="#root/_help_WOcw2SLH6tbX">server edition</a>.</p>
<h2>Layout basics</h2>
<p>Unlike the desktop version, the mobile version has a slightly different
UI meant to better fit the constrained screens of a mobile phone.</p>
<p>Here is a non-exhaustive list of differences between the desktop version
and the mobile one:</p>
<ul>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;is
displayed as a sidebar. To display the sidebar, press the button in the
top-left of the screen.</p>
<ul>
<li>
<p>There is also a swipe gesture that can be done from the left of the screen,
but the browser's navigation gesture interferes with it most of the time
(depending on the platform).</p>
</li>
<li>
<p>Press and hold a note to display the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/oPVyFC7WL2Lp/_help_YtSN43OrfzaA">Note tree contextual menu</a>.</p>
</li>
</ul>
</li>
<li>The&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_Ms1nauBra7gq">Quick search</a>&nbsp;bar
is also displayed at the top of the note tree.</li>
<li>The full&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/wArbEsdSae6g/_help_eIg8jdvaoNNd">Search</a>&nbsp;function
can be triggered either from either the&nbsp;<a class="reference-link"
href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>&nbsp;or
from the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
if configured.</li>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;is
displayed at the bottom of the screen.</p>
<ul>
<li>
<p>The launch bar uses a different configuration for icons than the desktop
version. See the dedicated page for more information on how to configure
it.</p>
</li>
</ul>
</li>
<li>
<p>Most of the note-related actions are grouped in the horizontal dots icon
on the top-right of the note.</p>
</li>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_3seOhtN8uLIY">Tabs</a>&nbsp;are
grouped under a tab switcher in the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>,
where the tabs are displayed in a full-screen grid with preview for easy
switching, as well as additional options such as reopening closed tabs.</p>
</li>
<li>
<p>Since v0.100.0,&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_luNhaphA37EO">Split View</a>&nbsp;can
also be used in mobile view, but with a maximum of two panes at once. The
splits are displayed vertically instead of horizontally.</p>
</li>
<li>
<p>Starting with v0.102.0, the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_IjZS7iK5EXtb">New Layout</a>&nbsp;is
enforced on mobile. This brings features such as the note badges, note
type switcher or collection properties which would otherwise not be available.</p>
</li>
</ul>
<h2>Installing as a PWA</h2>
<p>The mobile view can be set up as a PWA. While this does not offer any
offline capabilities, it will display the application in full-screen and
makes it easy to access via your mobile phone's home screen.</p>
<h3>On iOS with Safari</h3>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li
>Login.</li>
<li>
<p>Press the […] button in the bottom-right of the screen and select Share.</p>
</li>
<li>
<p>Scroll down to reveal the full list of items and choose “Add to Home Screen”.</p>
</li>
<li>
<p>Press “Add” and the web app will be available.</p>
</li>
</ol>
<h3>On Android with Google Chrome</h3>
<aside class="admonition important">
<p>Google Chrome requires the server to be served over HTTPS in order to
display in full-screen. If using HTTP, the app will appear like a normal
web page (similar to a bookmark).</p>
</aside>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li
>Login.</li>
<li>
<p>Press the three vertical dots icon in the top-right of the screen and
select <em>Add to Home screen.</em>
</p>
</li>
<li>
<p>Select the <em>Install</em> option.</p>
</li>
<li>
<p>Select an appropriate name.</p>
</li>
<li>
<p>The web app will appear as an application, not on the home screen.</p>
</li>
</ol>
<h3>On Android with Brave</h3>
<aside class="admonition important">
<p>Brave requires the server to be served over HTTPS in order to display
in full-screen. If using HTTP, the app will appear like a normal web page
(similar to a bookmark).</p>
</aside>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li
>Login.</li>
<li>
<p>Press the three vertical dots icon in the bottom-right of the screen and
select <em>Add to Home screen</em>.</p>
</li>
<li>
<p>Press the <em>Install</em> option.</p>
</li>
<li>
<p>The web app will appear as an application, not on the home screen.</p>
</li>
</ol>
<h3>On Samsung Browser</h3>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li
>Login.</li>
<li>
<p>Press the hamburger menu in the bottom-right of the screen.</p>
</li>
<li>
<p>Select <em>Add to</em>, followed by <em>Home screen</em>.</p>
</li>
<li>
<p>Press <em>Add</em> and the web app will appear on the home page.</p>
</li>
</ol>
<h2>Testing via the desktop application</h2>
<p>If you are running Trilium without a dedicated <a href="#root/_help_WOcw2SLH6tbX">server installation</a>,
you can still test the mobile application using the desktop application.
For more information, see&nbsp;<a class="reference-link" href="#root/_help_nRqcgfTb97uV">Using the desktop application as a server</a>.
To access it go to <code spellcheck="false">http://&lt;ip&gt;:37840/login?mobile</code> .</p>
<h2>Limitations</h2>
<p>Mobile frontend provides only some of the features of the full desktop
frontend:</p>
<ul>
<li>it is possible to browse the whole note tree, read and edit all types
of notes, but you can create only text notes</li>
<li>reading and editing <a href="#root/_help_bwg0e8ewQMak">protected notes</a> is
possible, but creating them is not supported</li>
<li>editing options is not supported</li>
<li>cloning notes is not supported</li>
<li>uploading file attachments is not supported</li>
</ul>
<h2>Forcing mobile/desktop frontend</h2>
<p>Trilium decides automatically whether to use mobile or desktop frontend.
<p>Trilium decides automatically whether to use mobile or desktop front-end.
If this is not appropriate, you can use <code spellcheck="false">?mobile</code> or
<code
spellcheck="false">?desktop</code>query param on <strong>login</strong> page (Note: you might
need to log out).</p>
<p>Alternatively, simply select <em>Switch to Mobile/Desktop Version</em> in
the&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_x3i7MxGccDuM">Global menu</a>.</p>
<h2>Scripting</h2>
<p>You can alter the behavior with <a href="#root/_help_CdNpE2pqjmI6">scripts</a> just
like for normal frontend. For script notes to be executed, they need to
have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>
<p>You can alter the behavior with&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/_help_CdNpE2pqjmI6">Scripting</a>,
just like for normal frontend. For script notes to be executed, they need
to have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>
<p>Custom&nbsp;<a class="reference-link" href="#root/pOsGYCXsbNQG/gh7bpGYxajRS/Vc8PjrjAGuOp/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;widgets
are also supported.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@ -3,7 +3,7 @@
import dateUtils from "../date_utils.js";
import path from "path";
import packageInfo from "../../../package.json" with { type: "json" };
import { getContentDisposition } from "../utils.js";
import { getContentDisposition, waitForStreamToFinish } from "../utils.js";
import protectedSessionService from "../protected_session.js";
import sanitize from "sanitize-filename";
import fs from "fs";
@ -468,6 +468,7 @@ async function exportToZip(taskContext: TaskContext<"export">, branch: BBranch,
taskContext.taskSucceeded(null);
}
async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath: string, zipExportOptions?: AdvancedExportOptions) {
const fileOutputStream = fs.createWriteStream(zipFilePath);
const taskContext = new TaskContext("no-progress-reporting", "export", null);
@ -479,6 +480,7 @@ async function exportToZipFile(noteId: string, format: ExportFormat, zipFilePath
}
await exportToZip(taskContext, note.getParentBranches()[0], format, fileOutputStream, false, zipExportOptions);
await waitForStreamToFinish(fileOutputStream);
log.info(`Exported '${noteId}' with format '${format}' to '${zipFilePath}'`);
}

View File

@ -40,18 +40,24 @@ export default function buildLaunchBarConfig() {
type: "launcher",
command: "showRecentChanges",
icon: "bx bx-history"
}
},
searchNotes: {
title: t("hidden-subtree.search-notes-title"),
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
},
bookmarks: {
title: t("hidden-subtree.bookmarks-title"),
type: "launcher",
builtinWidget: "bookmarks",
icon: "bx bx-bookmark"
},
};
const desktopAvailableLaunchers: HiddenSubtreeItem[] = [
{
id: "_lbBackInHistory",
...sharedLaunchers.backInHistory
},
{
id: "_lbForwardInHistory",
...sharedLaunchers.forwardInHistory
},
{ id: "_lbBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbForwardInHistory", ...sharedLaunchers.forwardInHistory },
{
id: "_commandPalette",
title: t("hidden-subtree.command-palette"),
@ -82,11 +88,7 @@ export default function buildLaunchBarConfig() {
},
{
id: "_lbSearch",
title: t("hidden-subtree.search-notes-title"),
type: "launcher",
command: "searchNotes",
icon: "bx bx-search",
attributes: [{ type: "label", name: "desktopOnly" }]
...sharedLaunchers.searchNotes
},
{
id: "_lbJumpTo",
@ -128,13 +130,7 @@ export default function buildLaunchBarConfig() {
baseSize: "50",
growthFactor: "0"
},
{
id: "_lbBookmarks",
title: t("hidden-subtree.bookmarks-title"),
type: "launcher",
builtinWidget: "bookmarks",
icon: "bx bx-bookmark"
},
{ id: "_lbBookmarks", ...sharedLaunchers.bookmarks },
{
id: "_lbToday",
...sharedLaunchers.openToday
@ -179,22 +175,15 @@ export default function buildLaunchBarConfig() {
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
{ id: "_lbMobileSearchNotes", ...sharedLaunchers.searchNotes },
{ id: "_lbMobileToday", ...sharedLaunchers.openToday },
{
id: "_lbMobileRecentChanges",
...sharedLaunchers.recentChanges
}
{ id: "_lbMobileRecentChanges", ...sharedLaunchers.recentChanges },
{ id: "_lbMobileBookmarks", ...sharedLaunchers.bookmarks }
];
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
{
id: "_lbMobileBackInHistory",
...sharedLaunchers.backInHistory
},
{
id: "_lbMobileForwardInHistory",
...sharedLaunchers.forwardInHistory
},
{ id: "_lbMobileBackInHistory", ...sharedLaunchers.backInHistory },
{ id: "_lbMobileForwardInHistory", ...sharedLaunchers.forwardInHistory },
{
id: "_lbMobileJumpTo",
title: t("hidden-subtree.jump-to-note-title"),
@ -210,7 +199,8 @@ export default function buildLaunchBarConfig() {
id: "_lbMobileTabSwitcher",
title: t("hidden-subtree.tab-switcher-title"),
type: "launcher",
builtinWidget: "mobileTabSwitcher"
builtinWidget: "mobileTabSwitcher",
icon: "bx bx-rectangle"
}
];

View File

@ -1,5 +1,4 @@
import chardet from "chardet";
import crypto from "crypto";
import escape from "escape-html";
@ -516,6 +515,13 @@ function slugify(text: string) {
.replace(/(^-|-$)+/g, ""); // trim dashes
}
export function waitForStreamToFinish(stream: any): Promise<void> {
return new Promise((resolve, reject) => {
stream.on("finish", () => resolve());
stream.on("error", (err) => reject(err));
});
}
export default {
compareVersions,
constantTimeCompare,
@ -556,5 +562,6 @@ export default {
toBase64,
toMap,
toObject,
unescapeHtml
unescapeHtml,
waitForStreamToFinish
};

View File

@ -1,5 +1,5 @@
# Documentation
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/y2NjgAW23yyj/Documentation_image.png" width="205" height="162">
There are multiple types of documentation for Trilium:<img class="image-style-align-right" src="api/images/qIpo9UiTZNdm/Documentation_image.png" width="205" height="162">
* The _User Guide_ represents the user-facing documentation. This documentation can be browsed by users directly from within Trilium, by pressing <kbd>F1</kbd>.
* The _Developer's Guide_ represents a set of Markdown documents that present the internals of Trilium, for developers.

View File

@ -1491,20 +1491,6 @@
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
"value": "bwg0e8ewQMak",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 40
},
{
"type": "label",
"name": "shareAlias",
@ -1518,11 +1504,90 @@
"value": "bx bx-mobile-alt",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "x3i7MxGccDuM",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "IjZS7iK5EXtb",
"isInheritable": false,
"position": 80
},
{
"type": "relation",
"name": "internalLink",
"value": "oPVyFC7WL2Lp",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "YtSN43OrfzaA",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "Ms1nauBra7gq",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "eIg8jdvaoNNd",
"isInheritable": false,
"position": 140
},
{
"type": "relation",
"name": "internalLink",
"value": "3seOhtN8uLIY",
"isInheritable": false,
"position": 150
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 160
},
{
"type": "relation",
"name": "internalLink",
"value": "luNhaphA37EO",
"isInheritable": false,
"position": 170
}
],
"format": "markdown",
"dataFileName": "Mobile Frontend.md",
"attachments": []
"attachments": [
{
"attachmentId": "JRliU5bOLszn",
"title": "IMG_1765.PNG",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Mobile Frontend_IMG_1765.PNG"
}
]
},
{
"isClone": false,
@ -2793,6 +2858,20 @@
"value": "tabs",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "RDslemsQ6gCp",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 60
}
],
"format": "markdown",
@ -2821,6 +2900,14 @@
"mime": "image/png",
"position": 10,
"dataFileName": "2_Tabs_image.png"
},
{
"attachmentId": "rFdKVhoVtsLA",
"title": "IMG_1767.PNG",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Tabs_IMG_1767.PNG"
}
]
},
@ -2840,20 +2927,6 @@
"type": "text",
"mime": "text/html",
"attributes": [
{
"type": "relation",
"name": "internalLink",
"value": "x0JgW8UqGXvq",
"isInheritable": false,
"position": 10
},
{
"type": "relation",
"name": "internalLink",
"value": "x3i7MxGccDuM",
"isInheritable": false,
"position": 20
},
{
"type": "relation",
"name": "internalLink",
@ -2861,41 +2934,6 @@
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "p9kXRFAkwN4o",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "OR8WJ7Iz9K4U",
"isInheritable": false,
"position": 50
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 60
},
{
"type": "relation",
"name": "internalLink",
"value": "SynTBQiBsdYJ",
"isInheritable": false,
"position": 70
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 80
},
{
"type": "label",
"name": "iconClass",
@ -2909,6 +2947,55 @@
"value": "launch-bar",
"isInheritable": false,
"position": 90
},
{
"type": "relation",
"name": "internalLink",
"value": "OFXdgB2nNk1F",
"isInheritable": false,
"position": 100
},
{
"type": "relation",
"name": "internalLink",
"value": "SynTBQiBsdYJ",
"isInheritable": false,
"position": 110
},
{
"type": "relation",
"name": "internalLink",
"value": "CdNpE2pqjmI6",
"isInheritable": false,
"position": 120
},
{
"type": "relation",
"name": "internalLink",
"value": "OR8WJ7Iz9K4U",
"isInheritable": false,
"position": 130
},
{
"type": "relation",
"name": "internalLink",
"value": "p9kXRFAkwN4o",
"isInheritable": false,
"position": 140
},
{
"type": "relation",
"name": "internalLink",
"value": "x3i7MxGccDuM",
"isInheritable": false,
"position": 150
},
{
"type": "relation",
"name": "internalLink",
"value": "x0JgW8UqGXvq",
"isInheritable": false,
"position": 160
}
],
"format": "markdown",
@ -5256,28 +5343,25 @@
"value": "bx bx-bookmarks",
"isInheritable": false,
"position": 30
},
{
"type": "relation",
"name": "internalLink",
"value": "xYmIYSP6wE3F",
"isInheritable": false,
"position": 40
},
{
"type": "relation",
"name": "internalLink",
"value": "x3i7MxGccDuM",
"isInheritable": false,
"position": 50
}
],
"format": "markdown",
"dataFileName": "Bookmarks.md",
"attachments": [
{
"attachmentId": "99dD0P74W8QJ",
"title": "bookmark-folder.png",
"role": "image",
"mime": "image/png",
"position": 10,
"dataFileName": "Bookmarks_bookmark-folder.png"
},
{
"attachmentId": "9dX71aLlSl9R",
"title": "bookmarks.gif",
"role": "image",
"mime": "image/gif",
"position": 10,
"dataFileName": "Bookmarks_bookmarks.gif"
}
]
"attachments": []
},
{
"isClone": false,

View File

@ -1,12 +1,18 @@
# Bookmarks
To easily access selected notes, you can bookmark them. See demo:
Frequently used notes can be bookmarked, which will make them appear in the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a> for easy access.
![](Bookmarks_bookmarks.gif)
## Configuring the launch bar
If bookmarks don't appear in the launch bar, then most likely the bookmark section has been hidden. Go to the <a class="reference-link" href="../UI%20Elements/Launch%20Bar.md">Launch Bar</a> configuration from the <a class="reference-link" href="../UI%20Elements/Global%20menu.md">Global menu</a> and ensure _Bookmarks_ is in the _Visible Launchers_ section.
## Bookmark folder
Space in the left panel is limited, and you might want to bookmark many items. One possible solution is to bookmark a folder, so it shows its children:
![](Bookmarks_bookmark-folder.png)
To do this, bookmark a folder and assign it the `#bookmarkFolder` label.
To do this, you need to add a `#bookmarkFolder` label to the note.
## Mobile
On mobile, bookmarks are only displayed starting with v0.102.0. Because of the more constrained screen size, the bookmarks are grouped under a single icon instead of displaying them as separate icons.
When pressed, a menu will appear listing all the bookmarks. Bookmark folders are also supported and will appear as sub-menus.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

View File

@ -1,7 +1,11 @@
# Launch Bar
## Position of the Launch bar
Depending on the layout selected, the launcher bar will either be on the left side of the screen with buttons displayed vertically or at the top of the screen. See [Vertical and horizontal layout](Vertical%20and%20horizontal%20layout.md) for more information.
On desktop, depending on the layout selected, the launcher bar will either be on the left side of the screen with buttons displayed vertically or at the top of the screen. See <a class="reference-link" href="Vertical%20and%20horizontal%20layout.md">Vertical and horizontal layout</a> for more information.
On mobile, the launch bar will always be at the bottom.
If there are too many items in the launch bar to fit the screen, it will become scrollable.
## Terminology
@ -9,30 +13,38 @@ Depending on the layout selected, the launcher bar will either be on the left si
* **Available Launcher**: a launcher that is not displayed on the launch bar, but can be added.
* **Visible Launcher**: a launcher that is currently displayed on the launch bar.
## Configuring the Launch bar
## Configuring the desktop Launch bar
There are two ways to configure the launch bar:
* Right click in the empty space between launchers on the launch bar and select _Configure Launchbar._
* Click on the [Global menu](Global%20menu.md) and select _Configure Launchbar_.
* Click on the <a class="reference-link" href="Global%20menu.md">Global menu</a> and select _Configure Launchbar_.
This will open a new tab with the [Note Tree](Note%20Tree.md) listing the launchers.
This will open a new tab with the <a class="reference-link" href="Note%20Tree.md">Note Tree</a> listing the launchers.
![](Launch%20Bar_image.png)
Expanding _Available Launchers_ section will show the list of launchers that are not displayed on the launch bar. The _Visible Launchers_ will show the ones that are currently displayed.
## Configuring the mobile launch bar
The launch bar on mobile uses a different configuration from the desktop one. The reasoning is that not all desktop icons are available on mobile, and fewer icons fit on a mobile screen.
To configure the launch bar on mobile, go to <a class="reference-link" href="Global%20menu.md">Global menu</a> and select _Configure Launchbar_.
The configure the mobile launch bar while on the desktop (especially useful to configure more complicated launchers such as scripts or custom widgets), go to <a class="reference-link" href="Global%20menu.md">Global menu</a> → Advanced → Show Hidden Subtree and look for the _Mobile Launch Bar_ section. While in the hidden subtree, it's also possible to drag launchers between the _Mobile Launch Bar_ and (Desktop) _Launch Bar_ sections.
### Adding/removing and reordering launchers
To display a new launcher in the launch bar, first look for it in the _Available Launchers_ section. Then right click it and select _Move to visible launchers_. It is also possible to drag and drop the item manually.
Similarly, to remove it from the launch bar, simply look for it in _Visible Launchers_ then right click it and select _Move to available launchers_ or use drag-and-drop.
Drag-and-drop the items in the tree in order to change their order. See [Note Tree](Note%20Tree.md) for more interaction options, including using keyboard shortcuts.
Drag-and-drop the items in the tree in order to change their order. See <a class="reference-link" href="Note%20Tree.md">Note Tree</a> for more interaction options, including using keyboard shortcuts.
## Customizing the launcher
* The icon of a launcher can be changed just like a normal note. See [Note Icons](../Notes/Note%20Icons.md) for more information.
* The icon of a launcher can be changed just like a normal note. See <a class="reference-link" href="../Notes/Note%20Icons.md">Note Icons</a> for more information.
* The title of the launcher can also be changed.
### Resetting
@ -45,19 +57,17 @@ Right click either the _Available launchers_ or _Visible launchers_ sections and
1. **Note Launcher**
A note launcher will simply navigate to a specified note.
1. Set the `target` promoted attribute to the note to navigate to.
2. Optionally, set `hoistedNote` to hoist a particular note. See [Note Hoisting](../Navigation/Note%20Hoisting.md) for more information.
2. Optionally, set `hoistedNote` to hoist a particular note. See <a class="reference-link" href="../Navigation/Note%20Hoisting.md">Note Hoisting</a> for more information.
3. Optionally, set a `keyboardShortcut` to trigger the launcher.
2. **Script Launcher**
An advanced launcher which will run a script upon pressing. See [Scripts](../../Scripting.md) for more information.
An advanced launcher which will run a script upon pressing. See <a class="reference-link" href="../../Scripting.md">Scripting</a> for more information.
1. Set `script` to point to the desired script to run.
2. Optionally, set a `keyboardShortcut` to trigger the launcher.
3. **Custom Widget**
Allows defining a custom widget to be rendered inside the launcher. See [Widget Basics](../../Scripting/Frontend%20Basics/Custom%20Widgets/Widget%20Basics.md) for more information.
Allows defining a custom widget to be rendered inside the launcher. See <a class="reference-link" href="../../Scripting/Frontend%20Basics/Custom%20Widgets/Widget%20Basics.md">Widget Basics</a> for more information.
4. **Spacers**
Launchers that create some distance between other launchers for better visual distinction.
Launchers are configured via predefined [Promoted Attributes](../../Advanced%20Usage/Attributes/Promoted%20Attributes.md).
Launchers are configured via predefined <a class="reference-link" href="../../Advanced%20Usage/Attributes/Promoted%20Attributes.md">Promoted Attributes</a>.

View File

@ -26,5 +26,20 @@ Since tabs are a commonly used feature, there are multiple keyboard shortcuts th
* <kbd>Ctrl</kbd>+<kbd>W</kbd> to close the current tab.
* <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>T</kbd> to reopen the last closed tab.
* <kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> to go to the next or previous tab.
* <kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to activate the first, second and up til ninth tab.
* There is also a shortcut to go to the last tab, but it is not assigned a key by default.
* <kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to activate the first, second and up to ninth tab.
* There is also a shortcut to go to the last tab, but it is not assigned a key by default.
## Mobile
<figure class="image image-style-align-right image_resized" style="width:34.12%;"><img style="aspect-ratio:1242/2688;" src="Tabs_IMG_1767.PNG" width="1242" height="2688"></figure>
Tabs are also supported on the <a class="reference-link" href="../../Installation%20%26%20Setup/Mobile%20Frontend.md">Mobile Frontend</a>.
Since v0.102.0, the tabs are displayed by pressing the dedicated tab switcher button in the <a class="reference-link" href="Launch%20Bar.md">Launch Bar</a>. In this view the tabs are laid out on a grid with a preview of the note content.
The context menu button at the top-right of the popup allows creating a new tab, reopening the last closed tab and closing all the tabs.
<a class="reference-link" href="Split%20View.md">Split View</a>s are also indicated in the tab switcher, with two titles displayed in a tab.
> [!NOTE]
> Versions prior to v0.102.0 also supported tabs, but they were displayed directly above the <a class="reference-link" href="Launch%20Bar.md">Launch Bar</a>. The decision to use a more mobile-like tab switcher was taken because the original tab bar could not support many tabs at once and the new design better aligns with how mobile applications handle tabs.

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View File

@ -1,28 +1,85 @@
# Mobile Frontend
Trilium ([server edition](Server%20Installation.md)) has a mobile web frontend which is optimized for touch based devices - smartphones and tablets. It is activated automatically during login process based on browser detection.
<figure class="image image_resized image-style-align-right" style="width:33.52%;"><img style="aspect-ratio:1242/2688;" src="Mobile Frontend_IMG_1765.PNG" width="1242" height="2688"></figure>
Mobile frontend is limited in features compared to full desktop frontend. See below for more details on this.
Trilium has a mobile web frontend which is optimized for touch based devices - smartphones and tablets. It is activated automatically during login process based on browser detection.
Note that this is not an Android/iOS app, this is just mobile friendly web page served on the [server edition](Server%20Installation.md).
Mobile frontend is limited in features compared to the full desktop version. See below for more details on this.
## Layout basics
Unlike the desktop version, the mobile version has a slightly different UI meant to better fit the constrained screens of a mobile phone.
Here is a non-exhaustive list of differences between the desktop version and the mobile one:
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree.md">Note Tree</a> is displayed as a sidebar. To display the sidebar, press the button in the top-left of the screen.
* There is also a swipe gesture that can be done from the left of the screen, but the browser's navigation gesture interferes with it most of the time (depending on the platform).
* Press and hold a note to display the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Note%20Tree/Note%20tree%20contextual%20menu.md">Note tree contextual menu</a>.
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Navigation/Quick%20search.md">Quick search</a> bar is also displayed at the top of the note tree.
* The full <a class="reference-link" href="../Basic%20Concepts%20and%20Features/Navigation/Search.md">Search</a> function can be triggered either from either the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Global%20menu.md">Global menu</a> or from the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>, if configured.
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a> is displayed at the bottom of the screen.
* The launch bar uses a different configuration for icons than the desktop version. See the dedicated page for more information on how to configure it.
* Most of the note-related actions are grouped in the horizontal dots icon on the top-right of the note.
* The <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Tabs.md">Tabs</a> are grouped under a tab switcher in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a>, where the tabs are displayed in a full-screen grid with preview for easy switching, as well as additional options such as reopening closed tabs.
* Since v0.100.0, <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Split%20View.md">Split View</a> can also be used in mobile view, but with a maximum of two panes at once. The splits are displayed vertically instead of horizontally.
* Starting with v0.102.0, the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/New%20Layout.md">New Layout</a> is enforced on mobile. This brings features such as the note badges, note type switcher or collection properties which would otherwise not be available.
## Installing as a PWA
The mobile view can be set up as a PWA. While this does not offer any offline capabilities, it will display the application in full-screen and makes it easy to access via your mobile phone's home screen.
### On iOS with Safari
1. Open your default web browser and access your Trilium instance.
2. Login.
3. Press the \[…\] button in the bottom-right of the screen and select Share.
4. Scroll down to reveal the full list of items and choose “Add to Home Screen”.
5. Press “Add” and the web app will be available.
### On Android with Google Chrome
> [!IMPORTANT]
> Google Chrome requires the server to be served over HTTPS in order to display in full-screen. If using HTTP, the app will appear like a normal web page (similar to a bookmark).
1. Open your default web browser and access your Trilium instance.
2. Login.
3. Press the three vertical dots icon in the top-right of the screen and select _Add to Home screen._
4. Select the _Install_ option.
5. Select an appropriate name.
6. The web app will appear as an application, not on the home screen.
### On Android with Brave
> [!IMPORTANT]
> Brave requires the server to be served over HTTPS in order to display in full-screen. If using HTTP, the app will appear like a normal web page (similar to a bookmark).
1. Open your default web browser and access your Trilium instance.
2. Login.
3. Press the three vertical dots icon in the bottom-right of the screen and select _Add to Home screen_.
4. Press the _Install_ option.
5. The web app will appear as an application, not on the home screen.
### On Samsung Browser
1. Open your default web browser and access your Trilium instance.
2. Login.
3. Press the hamburger menu in the bottom-right of the screen.
4. Select _Add to_, followed by _Home screen_.
5. Press _Add_ and the web app will appear on the home page.
## Testing via the desktop application
If you are running Trilium without a dedicated [server installation](Server%20Installation.md), you can still test the mobile application using the desktop application. For more information, see <a class="reference-link" href="Desktop%20Installation/Using%20the%20desktop%20application%20.md">Using the desktop application as a server</a>. To access it go to `http://<ip>:37840/login?mobile` .
## Limitations
Mobile frontend provides only some of the features of the full desktop frontend:
* it is possible to browse the whole note tree, read and edit all types of notes, but you can create only text notes
* reading and editing [protected notes](../Basic%20Concepts%20and%20Features/Notes/Protected%20Notes.md) is possible, but creating them is not supported
* editing options is not supported
* cloning notes is not supported
* uploading file attachments is not supported
## Forcing mobile/desktop frontend
Trilium decides automatically whether to use mobile or desktop frontend. If this is not appropriate, you can use `?mobile` or `?desktop` query param on **login** page (Note: you might need to log out).
Trilium decides automatically whether to use mobile or desktop front-end. If this is not appropriate, you can use `?mobile` or `?desktop` query param on **login** page (Note: you might need to log out).
Alternatively, simply select _Switch to Mobile/Desktop Version_ in the <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Global%20menu.md">Global menu</a>.
## Scripting
You can alter the behavior with [scripts](../Scripting.md) just like for normal frontend. For script notes to be executed, they need to have labeled `#run=mobileStartup`.
You can alter the behavior with <a class="reference-link" href="../Scripting.md">Scripting</a>, just like for normal frontend. For script notes to be executed, they need to have labeled `#run=mobileStartup`.
Custom <a class="reference-link" href="../Basic%20Concepts%20and%20Features/UI%20Elements/Launch%20Bar.md">Launch Bar</a> widgets are also supported.

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

View File

@ -113,7 +113,7 @@
[
moreutils # sponge
nodejs.python
removeReferencesTo
removeReferencesTo
]
++ lib.optionals (app == "desktop" || app == "edit-docs") [
copyDesktopItems
@ -126,7 +126,7 @@
which
electron
]
++ lib.optionals (app == "server") [
++ lib.optionals (app == "server" || app == "build-docs") [
makeBinaryWrapper
]
++ lib.optionals stdenv.hostPlatform.isDarwin [
@ -153,7 +153,7 @@
# This file is a symlink into /build which is not allowed.
postFixup = ''
rm $out/opt/trilium*/node_modules/better-sqlite3/node_modules/.bin/prebuild-install || true
find $out/opt -name prebuild-install -path "*/better-sqlite3/node_modules/.bin/*" -delete || true
'';
components = [
@ -169,6 +169,7 @@
"packages/highlightjs"
"packages/turndown-plugin-gfm"
"apps/build-docs"
"apps/client"
"apps/db-compare"
"apps/desktop"
@ -277,11 +278,46 @@
'';
};
build-docs = makeApp {
app = "build-docs";
preBuildCommands = ''
pushd apps/server
pnpm rebuild || true
popd
'';
buildTask = "client:build && pnpm run server:build && pnpm run --filter build-docs build";
mainProgram = "trilium-build-docs";
installCommands = ''
mkdir -p $out/{bin,opt/trilium-build-docs}
# Copy build-docs dist
cp --archive apps/build-docs/dist/* $out/opt/trilium-build-docs
# Copy server dist (needed for runtime)
mkdir -p $out/opt/trilium-build-docs/server
cp --archive apps/server/dist/* $out/opt/trilium-build-docs/server/
# Copy client dist (needed for runtime)
mkdir -p $out/opt/trilium-build-docs/client
cp --archive apps/client/dist/* $out/opt/trilium-build-docs/client/
# Copy share-theme (needed for exports)
mkdir -p $out/opt/trilium-build-docs/packages/share-theme
cp --archive packages/share-theme/dist/* $out/opt/trilium-build-docs/packages/share-theme/
# Create wrapper script
makeWrapper ${lib.getExe nodejs} $out/bin/trilium-build-docs \
--add-flags $out/opt/trilium-build-docs/cli.cjs \
--set TRILIUM_RESOURCE_DIR $out/opt/trilium-build-docs/server
'';
};
in
{
packages.desktop = desktop;
packages.server = server;
packages.edit-docs = edit-docs;
packages.build-docs = build-docs;
packages.default = desktop;

6
pnpm-lock.yaml generated
View File

@ -164,6 +164,9 @@ importers:
fs-extra:
specifier: 11.3.3
version: 11.3.3
js-yaml:
specifier: 4.1.1
version: 4.1.1
react:
specifier: 19.2.4
version: 19.2.4
@ -16534,8 +16537,6 @@ snapshots:
'@ckeditor/ckeditor5-ui': 47.4.0
'@ckeditor/ckeditor5-utils': 47.4.0
ckeditor5: 47.4.0
transitivePeerDependencies:
- supports-color
'@ckeditor/ckeditor5-markdown-gfm@47.4.0':
dependencies:
@ -16946,6 +16947,7 @@ snapshots:
ckeditor5: 47.4.0
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@ckeditor/ckeditor5-utils@47.4.0':