diff --git a/apps/web/src/components/EditorPane.tsx b/apps/web/src/components/EditorPane.tsx
index d0eb4df0..d9855a1f 100644
--- a/apps/web/src/components/EditorPane.tsx
+++ b/apps/web/src/components/EditorPane.tsx
@@ -126,6 +126,7 @@ import {
EDITOR_LOCAL_SAVE_DELAY_MS,
getEditableMemoTitle,
getNotebookMoveOptions,
+ type EditorContentAlignment,
type MemoDocumentActionRequest,
} from "@/lib/app-helpers";
import { copyEditorToWeChat, copyMarkdownToWeChat } from "@/lib/wechat-copy";
@@ -604,6 +605,7 @@ type EditorPaneProps = {
repository: EdgeEverRepository;
desktopFocusMode: boolean;
onToggleDesktopFocusMode: () => void;
+ editorContentAlignment: EditorContentAlignment;
mobileDefaultEditMemoId: string | null;
preserveUnsavedContentFromMemoId?: string | null;
saveBlocked?: boolean;
@@ -670,6 +672,7 @@ const RichEditorPane = ({
repository,
desktopFocusMode,
onToggleDesktopFocusMode,
+ editorContentAlignment,
mobileDefaultEditMemoId,
preserveUnsavedContentFromMemoId: _preserveUnsavedContentFromMemoId,
saveBlocked: _saveBlocked = false,
@@ -4118,7 +4121,9 @@ const RichEditorPane = ({
: "min-h-full items-start px-6 py-6 sm:px-10",
desktopFocusMode
? "mx-auto w-full max-w-[1400px] justify-center"
- : "w-full justify-center"
+ : editorContentAlignment === "center"
+ ? "w-full justify-center"
+ : "w-full justify-start"
)}
>
{
+ test("uses a Radix submenu for notebook moves so opening the picker does not close the menu", () => {
+ const source = readFileSync(new URL("./MemoListPane.tsx", import.meta.url), "utf8");
+
+ expect(source).toContain("
");
+ expect(source).toContain(" {
const { t } = useTranslation();
const [memoContextMenu, setMemoContextMenu] = useState(null);
- const [contextMoveOpen, setContextMoveOpen] = useState(false);
const [listDensity, setListDensity] = useState(() => readMemoListDensityPreference());
const [lastSelectedMemoId, setLastSelectedMemoId] = useState(null);
const [moveTargetNotebookId, setMoveTargetNotebookId] = useState("");
@@ -740,7 +741,6 @@ export const MemoListPane = ({
setDesktopActionsOpen(false);
setDesktopFilterOpen(false);
setDesktopSortOpen(false);
- setContextMoveOpen(false);
setMemoContextMenu(null);
setMobileListActionsOpen(false);
setMobileMoveOpen(false);
@@ -786,7 +786,6 @@ export const MemoListPane = ({
const x = Math.min(clientX, Math.max(12, window.innerWidth - menuWidth - 12));
const y = Math.min(clientY, Math.max(12, window.innerHeight - menuHeight - 12));
- setContextMoveOpen(false);
setMemoContextMenu({ memo, x, y });
};
@@ -804,7 +803,6 @@ export const MemoListPane = ({
}
event.preventDefault();
- setContextMoveOpen(false);
setMemoContextMenu(null);
};
@@ -817,7 +815,6 @@ export const MemoListPane = ({
handleToggleMemo(memo.id);
}
- setContextMoveOpen(false);
setMemoContextMenu(null);
};
@@ -1522,40 +1519,36 @@ export const MemoListPane = ({
>
) : (
<>
- setContextMoveOpen((value) => !value)}
- >
-
- {t("memoList.moveToNotebook")}
-
-
- {contextMoveOpen && (
-
- {moveNotebookOptions.map((option: any) => (
-
+
))}
-
- )}
+
+
(null);
const [notebookSortMode, setNotebookSortMode] = useState(readNotebookSortPreference);
+ const handleMoveNotebook = useCallback((notebookId: string, targetNotebookId: string, position: NotebookDropPosition) => {
+ setNotebookSortMode("custom");
+ onMoveNotebook(notebookId, targetNotebookId, position);
+ }, [onMoveNotebook]);
+
const stopNotebookDragAutoScroll = useCallback(() => {
if (notebookDragScrollFrameRef.current === null) {
return;
@@ -550,16 +555,24 @@ export const NotebookPane = ({
-
-
-
+
+
+
+
+
+
+
+
+ {t("notebookPane.sortTitle", { label: activeNotebookSortLabel })}
+
+
+
{notebookSortOptions.map((option) => (
void;
shortcutSettings: ShortcutSettings;
onShortcutSettingsChange: (settings: ShortcutSettings) => void;
+ editorContentAlignment: EditorContentAlignment;
+ onEditorContentAlignmentChange: (alignment: EditorContentAlignment) => void;
onLogout: () => void;
isLoggingOut: boolean;
authRequired: boolean;
@@ -93,6 +95,8 @@ export const SettingsPane = ({
onSyncIntervalChange,
shortcutSettings,
onShortcutSettingsChange,
+ editorContentAlignment,
+ onEditorContentAlignmentChange,
onLogout,
isLoggingOut,
authRequired,
@@ -214,6 +218,8 @@ export const SettingsPane = ({
onSyncIntervalChange={onSyncIntervalChange}
shortcutSettings={shortcutSettings}
onShortcutSettingsChange={onShortcutSettingsChange}
+ editorContentAlignment={editorContentAlignment}
+ onEditorContentAlignmentChange={onEditorContentAlignmentChange}
/>
diff --git a/apps/web/src/components/WorkspaceApp.tsx b/apps/web/src/components/WorkspaceApp.tsx
index 0302d7e0..020000d7 100644
--- a/apps/web/src/components/WorkspaceApp.tsx
+++ b/apps/web/src/components/WorkspaceApp.tsx
@@ -761,10 +761,12 @@ export const WorkspaceApp = ({
const [multiSelectKeyDown, setMultiSelectKeyDown] = useState(false);
const {
desktopFocusMode,
+ editorContentAlignment,
imageCompressionEnabled,
memoListWidth,
resetMemoListWidth,
setDesktopFocusMode,
+ setEditorContentAlignment,
setImageCompressionEnabled,
setMemoListWidth,
setShortcutSettings,
@@ -2965,6 +2967,8 @@ export const WorkspaceApp = ({
onSyncIntervalChange={setSyncIntervalMs}
shortcutSettings={shortcutSettings}
onShortcutSettingsChange={setShortcutSettings}
+ editorContentAlignment={editorContentAlignment}
+ onEditorContentAlignmentChange={setEditorContentAlignment}
onLogout={onLogout}
isLoggingOut={isLoggingOut}
authRequired={authRequired}
@@ -3011,6 +3015,7 @@ export const WorkspaceApp = ({
onOpenAiPrompts={handleOpenAiPrompts}
desktopFocusMode={desktopFocusModeActive}
onToggleDesktopFocusMode={toggleDesktopFocusMode}
+ editorContentAlignment={editorContentAlignment}
mobileDefaultEditMemoId={createdMemoEditId}
isTrashView={memoView === "trash"}
notebooks={notebooks}
diff --git a/apps/web/src/components/settings/PreferenceCard.test.mjs b/apps/web/src/components/settings/PreferenceCard.test.mjs
new file mode 100644
index 00000000..1515f6ac
--- /dev/null
+++ b/apps/web/src/components/settings/PreferenceCard.test.mjs
@@ -0,0 +1,13 @@
+import { describe, expect, test } from "bun:test";
+import { readFileSync } from "node:fs";
+
+describe("editor content alignment preference", () => {
+ test("is configured from settings instead of the per-note toolbar", () => {
+ const preferenceCard = readFileSync(new URL("./PreferenceCard.tsx", import.meta.url), "utf8");
+ const editorPane = readFileSync(new URL("../EditorPane.tsx", import.meta.url), "utf8");
+
+ expect(preferenceCard).toContain('t("settings.editorContentAlignmentTitle")');
+ expect(preferenceCard).toContain('onEditorContentAlignmentChange(value as EditorContentAlignment)');
+ expect(editorPane).not.toContain("onToggleEditorContentAlignment");
+ });
+});
diff --git a/apps/web/src/components/settings/PreferenceCard.tsx b/apps/web/src/components/settings/PreferenceCard.tsx
index 1f70d351..49a1092b 100644
--- a/apps/web/src/components/settings/PreferenceCard.tsx
+++ b/apps/web/src/components/settings/PreferenceCard.tsx
@@ -1,7 +1,7 @@
-import { ChartNoAxesCombined, Image, Languages, MousePointerClick, Palette, RefreshCw, Sparkles } from "lucide-react";
+import { AlignHorizontalJustifyCenter, ChartNoAxesCombined, Image, Languages, MousePointerClick, Palette, RefreshCw, Sparkles } from "lucide-react";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
-import { writeSyncIntervalPreference, type ShortcutSettings, type SyncIntervalPreference } from "@/lib/app-helpers";
+import { writeSyncIntervalPreference, type EditorContentAlignment, type ShortcutSettings, type SyncIntervalPreference } from "@/lib/app-helpers";
import {
EDITOR_LINK_OPEN_MODE_CHANGED_EVENT,
getStoredEditorLinkOpenMode,
@@ -42,6 +42,8 @@ interface PreferenceCardProps {
onSyncIntervalChange: (intervalMs: number | null) => void;
shortcutSettings: ShortcutSettings;
onShortcutSettingsChange: (settings: ShortcutSettings) => void;
+ editorContentAlignment: EditorContentAlignment;
+ onEditorContentAlignmentChange: (alignment: EditorContentAlignment) => void;
}
export const PreferenceCard = ({
@@ -51,6 +53,8 @@ export const PreferenceCard = ({
onSyncIntervalChange,
shortcutSettings,
onShortcutSettingsChange,
+ editorContentAlignment,
+ onEditorContentAlignmentChange,
}: PreferenceCardProps) => {
const { t } = useTranslation();
const {
@@ -194,6 +198,30 @@ export const PreferenceCard = ({
+
+
+
+
+
{t("settings.editorContentAlignmentTitle")}
+
{t("settings.editorContentAlignmentDescription")}
+
+
+
+
+
+
+
diff --git a/apps/web/src/components/ui/dropdown-menu.tsx b/apps/web/src/components/ui/dropdown-menu.tsx
index 2be13008..0cfb6be9 100644
--- a/apps/web/src/components/ui/dropdown-menu.tsx
+++ b/apps/web/src/components/ui/dropdown-menu.tsx
@@ -36,14 +36,16 @@ const DropdownMenuSubContent = React.forwardRef<
React.ElementRef
,
React.ComponentPropsWithoutRef
>(({ className, ...props }, ref) => (
-
+
+
+
));
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName;
diff --git a/apps/web/src/hooks/useWorkspacePreferences.ts b/apps/web/src/hooks/useWorkspacePreferences.ts
index 3477e2f6..134d99ae 100644
--- a/apps/web/src/hooks/useWorkspacePreferences.ts
+++ b/apps/web/src/hooks/useWorkspacePreferences.ts
@@ -3,16 +3,19 @@ import {
DEFAULT_MEMO_LIST_WIDTH_PX,
clampMemoListWidth,
readDesktopFocusModePreference,
+ readEditorContentAlignmentPreference,
readImageCompressionPreference,
readMemoListWidthPreference,
readShortcutSettingsPreference,
readSyncIntervalPreference,
writeDesktopFocusModePreference,
+ writeEditorContentAlignmentPreference,
writeImageCompressionPreference,
writeMemoListWidthPreference,
writeShortcutSettingsPreference,
writeSyncIntervalPreference,
type ShortcutSettings,
+ type EditorContentAlignment,
} from "@/lib/app-helpers";
export type SyncIntervalPreference = "off" | "30s" | "5m" | "15m" | "30m" | "1h" | "2h";
@@ -33,6 +36,7 @@ export const useWorkspacePreferences = () => {
const [imageCompressionEnabled, setImageCompressionEnabled] = useState(readImageCompressionPreference);
const [syncIntervalMs, setSyncIntervalMs] = useState(readSyncIntervalPreference);
const [desktopFocusMode, setDesktopFocusModeState] = useState(readDesktopFocusModePreference);
+ const [editorContentAlignment, setEditorContentAlignmentState] = useState(readEditorContentAlignmentPreference);
const [shortcutSettings, setShortcutSettings] = useState(readShortcutSettingsPreference);
const [memoListWidth, setMemoListWidthState] = useState(readMemoListWidthPreference);
@@ -45,6 +49,11 @@ export const useWorkspacePreferences = () => {
writeDesktopFocusModePreference(enabled);
}, []);
+ const setEditorContentAlignment = useCallback((alignment: EditorContentAlignment) => {
+ setEditorContentAlignmentState(alignment);
+ writeEditorContentAlignmentPreference(alignment);
+ }, []);
+
const setMemoListWidth = useCallback((width: number) => {
const nextWidth = clampMemoListWidth(width);
setMemoListWidthState(nextWidth);
@@ -57,10 +66,12 @@ export const useWorkspacePreferences = () => {
return {
desktopFocusMode,
+ editorContentAlignment,
imageCompressionEnabled,
memoListWidth,
resetMemoListWidth,
setDesktopFocusMode,
+ setEditorContentAlignment,
setImageCompressionEnabled,
setMemoListWidth,
setShortcutSettings,
diff --git a/apps/web/src/lib/app-helpers.test.mjs b/apps/web/src/lib/app-helpers.test.mjs
index 0047bb05..2017958c 100644
--- a/apps/web/src/lib/app-helpers.test.mjs
+++ b/apps/web/src/lib/app-helpers.test.mjs
@@ -2,9 +2,16 @@ import { afterEach, describe, expect, test } from "bun:test";
import {
DEFAULT_SYNC_INTERVAL_MS,
DESKTOP_FOCUS_MODE_STORAGE_KEY,
+ EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY,
+ NOTEBOOK_SORT_STORAGE_KEY,
SYNC_INTERVAL_STORAGE_KEY,
+ getNotebookSortComparator,
+ readEditorContentAlignmentPreference,
+ readNotebookSortPreference,
readSyncIntervalPreference,
readDesktopFocusModePreference,
+ writeEditorContentAlignmentPreference,
+ writeNotebookSortPreference,
writeSyncIntervalPreference,
writeDesktopFocusModePreference,
} from "./app-helpers.ts";
@@ -70,6 +77,66 @@ describe("desktop focus mode preference", () => {
});
});
+describe("editor content alignment preference", () => {
+ test("defaults to left aligned and persists both supported alignments", () => {
+ const values = installLocalStorage();
+ expect(readEditorContentAlignmentPreference()).toBe("start");
+
+ writeEditorContentAlignmentPreference("start");
+ expect(values.get(EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY)).toBe("start");
+ expect(readEditorContentAlignmentPreference()).toBe("start");
+
+ writeEditorContentAlignmentPreference("center");
+ expect(values.get(EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY)).toBe("center");
+ expect(readEditorContentAlignmentPreference()).toBe("center");
+ });
+
+ test("falls back to left aligned for unknown or unavailable storage", () => {
+ const values = installLocalStorage();
+ values.set(EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY, "unexpected");
+ expect(readEditorContentAlignmentPreference()).toBe("start");
+
+ globalThis.window = {
+ localStorage: {
+ getItem: () => {
+ throw new Error("blocked");
+ },
+ },
+ };
+ expect(readEditorContentAlignmentPreference()).toBe("start");
+ });
+});
+
+describe("custom notebook sorting", () => {
+ test("persists the custom sort mode", () => {
+ const values = installLocalStorage();
+ writeNotebookSortPreference("custom");
+ expect(values.get(NOTEBOOK_SORT_STORAGE_KEY)).toBe("custom");
+ expect(readNotebookSortPreference()).toBe("custom");
+ });
+
+ test("orders notebooks by persisted sort order with a stable name fallback", () => {
+ const compare = getNotebookSortComparator("custom");
+ const base = {
+ parentId: null,
+ slug: null,
+ icon: null,
+ color: null,
+ memoCount: 0,
+ lastMemoUpdatedAt: null,
+ createdAt: "2026-01-01T00:00:00.000Z",
+ updatedAt: "2026-01-01T00:00:00.000Z",
+ };
+ const notebooks = [
+ { ...base, id: "third", name: "C", sortOrder: 30 },
+ { ...base, id: "second", name: "B", sortOrder: 20 },
+ { ...base, id: "first", name: "A", sortOrder: 20 },
+ ];
+
+ expect(notebooks.sort(compare).map((item) => item.id)).toEqual(["first", "second", "third"]);
+ });
+});
+
describe("automatic sync interval preference", () => {
test("defaults to 30 seconds", () => {
installLocalStorage();
diff --git a/apps/web/src/lib/app-helpers.ts b/apps/web/src/lib/app-helpers.ts
index fd143dec..fa589c8e 100644
--- a/apps/web/src/lib/app-helpers.ts
+++ b/apps/web/src/lib/app-helpers.ts
@@ -10,7 +10,8 @@ export type Pane = "notebooks" | "memos" | "editor";
export type MemoView = "notebook" | "trash";
export type MemoFilterMode = "all" | "tagged" | "untagged" | "pinned";
export type MemoSortMode = "updated-desc" | "created-desc" | "title-asc";
-export type NotebookSortMode = "name-asc" | "memo-count-desc" | "updated-desc";
+export type NotebookSortMode = "custom" | "name-asc" | "memo-count-desc" | "updated-desc";
+export type EditorContentAlignment = "start" | "center";
export type MemoListDensity = "preview" | "compact";
export type ShortcutAction = "createMemo" | "createNotebook" | "focusSearch" | "focusReplace";
export type ShortcutBinding = {
@@ -109,6 +110,7 @@ export const IMAGE_COMPRESSION_STORAGE_KEY = "edgeever.imageCompressionEnabled";
export const SYNC_INTERVAL_STORAGE_KEY = "edgeever.syncInterval";
const LEGACY_AUTO_SAVE_INTERVAL_STORAGE_KEY = "edgeever.autoSaveInterval";
export const DESKTOP_FOCUS_MODE_STORAGE_KEY = "edgeever.desktopFocusMode";
+export const EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY = "edgeever.editorContentAlignment";
export const MEMO_LIST_DENSITY_STORAGE_KEY = "edgeever.memoListDensity";
export const MEMO_LIST_WIDTH_STORAGE_KEY = "edgeever.memoListWidth";
export const NOTEBOOK_SORT_STORAGE_KEY = "edgeever.notebookSort";
@@ -139,9 +141,10 @@ export const getMemoSortOptions = (t: TFunction): Array<{ value: MemoSortMode; l
{ value: "title-asc", label: t("options.memoSort.titleAsc") },
];
-const NOTEBOOK_SORT_VALUES: NotebookSortMode[] = ["name-asc", "memo-count-desc", "updated-desc"];
+const NOTEBOOK_SORT_VALUES: NotebookSortMode[] = ["custom", "name-asc", "memo-count-desc", "updated-desc"];
export const getNotebookSortOptions = (t: TFunction): Array<{ value: NotebookSortMode; label: string }> => [
+ { value: "custom", label: t("options.notebookSort.custom") },
{ value: "name-asc", label: t("options.notebookSort.nameAsc") },
{ value: "memo-count-desc", label: t("options.notebookSort.memoCountDesc") },
{ value: "updated-desc", label: t("options.notebookSort.updatedDesc") },
@@ -275,6 +278,22 @@ export const writeDesktopFocusModePreference = (enabled: boolean) => {
}
};
+export const readEditorContentAlignmentPreference = (): EditorContentAlignment => {
+ try {
+ return window.localStorage.getItem(EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY) === "center" ? "center" : "start";
+ } catch {
+ return "start";
+ }
+};
+
+export const writeEditorContentAlignmentPreference = (alignment: EditorContentAlignment) => {
+ try {
+ window.localStorage.setItem(EDITOR_CONTENT_ALIGNMENT_STORAGE_KEY, alignment);
+ } catch {
+ // Local storage can be unavailable in private or restricted browser contexts.
+ }
+};
+
export const readMemoListDensityPreference = (): MemoListDensity => {
try {
const density = window.localStorage.getItem(MEMO_LIST_DENSITY_STORAGE_KEY);
@@ -457,6 +476,10 @@ const compareNotebookUpdatedDesc = (first: Notebook, second: Notebook) => {
};
export const getNotebookSortComparator = (sortMode: NotebookSortMode): NotebookNodeComparator => {
+ if (sortMode === "custom") {
+ return (first, second) => first.sortOrder - second.sortOrder || compareNotebookNameAsc(first, second);
+ }
+
if (sortMode === "memo-count-desc") {
return (first, second) => second.memoCount - first.memoCount || compareNotebookNameAsc(first, second);
}
diff --git a/packages/shared/src/i18n/en-US.ts b/packages/shared/src/i18n/en-US.ts
index 1008aca1..61d26044 100644
--- a/packages/shared/src/i18n/en-US.ts
+++ b/packages/shared/src/i18n/en-US.ts
@@ -204,6 +204,12 @@ export const enUS = {
mermaidThemeDescription: "Choose the visual theme for diagrams in notes.",
editorThemeTitle: "Editor theme",
editorThemeDescription: "Choose a writing style for the note editor. Saved in this browser.",
+ editorContentAlignmentTitle: "Note content layout",
+ editorContentAlignmentDescription: "Choose how note content is displayed across all notes on desktop. Saved in this browser.",
+ editorContentAlignments: {
+ center: "Optimize readability (centered)",
+ start: "Align left",
+ },
editorThemes: {
default: "System default",
"minimal-emerald": "Minimal Emerald",
@@ -695,6 +701,7 @@ export const enUS = {
titleAsc: "Title A-Z",
},
notebookSort: {
+ custom: "Custom",
nameAsc: "Name",
memoCountDesc: "Note count",
updatedDesc: "Updated date",
diff --git a/packages/shared/src/i18n/zh-CN.ts b/packages/shared/src/i18n/zh-CN.ts
index 559502c0..443c68fe 100644
--- a/packages/shared/src/i18n/zh-CN.ts
+++ b/packages/shared/src/i18n/zh-CN.ts
@@ -204,6 +204,12 @@ export const zhCN = {
mermaidThemeDescription: "选择笔记中图表的视觉主题。",
editorThemeTitle: "编辑器主题",
editorThemeDescription: "选择正文编辑区的排版风格,偏好会保存在当前浏览器中。",
+ editorContentAlignmentTitle: "笔记正文布局",
+ editorContentAlignmentDescription: "选择桌面端所有笔记的正文显示方式,偏好会保存在当前浏览器中。",
+ editorContentAlignments: {
+ center: "优化可读性(居中)",
+ start: "靠左显示",
+ },
editorThemes: {
default: "系统默认",
"minimal-emerald": "minimal品牌绿",
@@ -695,6 +701,7 @@ export const zhCN = {
titleAsc: "标题 A-Z",
},
notebookSort: {
+ custom: "自定义",
nameAsc: "名称",
memoCountDesc: "笔记数量",
updatedDesc: "更新日期",