Update browser modal to side-panel view

The browser interface has been refactored from a modal to a persistent side-panel view that can be opened and closed
like other Obsidian panes. This provides better workflow integration and allows users to keep the browser open while
working with other notes.

The documentation has been updated to clarify that the "Browse reMarkable" command now opens the side-panel browser
instead of a modal dialog.
This commit is contained in:
2026-05-31 18:43:35 +02:00
parent 23ee2314b3
commit 7a6cc78e17
8 changed files with 192 additions and 85 deletions
+107
View File
@@ -0,0 +1,107 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ButtonComponent } from "obsidian";
import RemarkablePlugin from "../src/main";
import { REMARKABLE_BROWSER_VIEW_TYPE, RemarkableBrowserView } from "../src/ui/browser-view";
import { RmapiNode } from "../src/types";
function createDocument(name: string): RmapiNode {
return {
id: `id-${name}`,
name,
type: "DocumentType",
version: 1,
modifiedClient: "2026-05-31T10:00:00Z",
currentPage: 0,
parent: "",
tags: [],
starred: false,
};
}
function createPlugin(nodes: RmapiNode[]) {
const calls: any[] = [];
const plugin: any = {
app: {},
rmapi: {
list: async () => nodes,
},
downloader: {
downloadDocument: async (...args: any[]) => {
calls.push(args);
},
},
};
return { calls, plugin };
}
test("RemarkableBrowserView creates import and handwriting buttons for documents", async () => {
ButtonComponent.instances = [];
const doc = createDocument("Meeting Notes");
const { calls, plugin } = createPlugin([doc]);
const view = new RemarkableBrowserView({} as any, plugin);
await view.onOpen();
const importButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Import");
const markdownButton = ButtonComponent.instances.find((button) => button.buttonEl.text === "Handwriting to Markdown");
assert.ok(importButton);
assert.ok(markdownButton);
await importButton.click();
await markdownButton.click();
assert.deepEqual(calls, [
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: false, forceDownload: true }],
[{ node: doc, remotePath: "/Meeting Notes" }, { convertToMd: true, forceDownload: true }],
]);
});
test("openRemarkableBrowser opens the browser view in the right sidebar", async () => {
const calls: any[] = [];
const leaf = {
setViewState: async (state: any) => {
calls.push(["setViewState", state]);
},
};
const plugin: any = new RemarkablePlugin();
plugin.app = {
workspace: {
getLeavesOfType: () => [],
getRightLeaf: () => leaf,
revealLeaf: async (targetLeaf: any) => {
calls.push(["revealLeaf", targetLeaf]);
},
},
};
await plugin.openRemarkableBrowser();
assert.deepEqual(calls, [
["setViewState", { type: REMARKABLE_BROWSER_VIEW_TYPE, active: true }],
["revealLeaf", leaf],
]);
});
test("openRemarkableBrowser reveals an existing browser leaf", async () => {
const calls: any[] = [];
const existingLeaf = {};
const plugin: any = new RemarkablePlugin();
plugin.app = {
workspace: {
getLeavesOfType: () => [existingLeaf],
getRightLeaf: () => {
throw new Error("should not create a new leaf");
},
revealLeaf: async (targetLeaf: any) => {
calls.push(targetLeaf);
},
},
};
await plugin.openRemarkableBrowser();
assert.deepEqual(calls, [existingLeaf]);
});