1 parent
c175cba97b
commit
e59ea58971
14 files changed
+244
-51
No files matched your search
@@ -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"
|
||||
)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
|
||||
describe("memo context menu", () => {
|
||||
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("<DropdownMenuSub>");
|
||||
expect(source).toContain("<DropdownMenuSubTrigger");
|
||||
expect(source).toContain("<DropdownMenuSubContent");
|
||||
expect(source).not.toContain("contextMoveOpen");
|
||||
});
|
||||
});
|
||||
@@ -15,7 +15,6 @@ import * as m from "motion/react-m";
|
||||
import {
|
||||
X,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Search,
|
||||
MoreHorizontal,
|
||||
@@ -54,6 +53,9 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
@@ -473,7 +475,6 @@ export const MemoListPane = ({
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [memoContextMenu, setMemoContextMenu] = useState<MemoContextMenuState | null>(null);
|
||||
const [contextMoveOpen, setContextMoveOpen] = useState(false);
|
||||
const [listDensity, setListDensity] = useState<MemoListDensity>(() => readMemoListDensityPreference());
|
||||
const [lastSelectedMemoId, setLastSelectedMemoId] = useState<string | null>(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 = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
className="flex h-9 w-full items-center gap-2 px-3 text-left text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
|
||||
disabled={moveNotebookOptions.length === 0}
|
||||
onClick={() => setContextMoveOpen((value) => !value)}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
<span className="min-w-0 flex-1 truncate">{t("memoList.moveToNotebook")}</span>
|
||||
<ChevronRight className={cn("h-4 w-4 transition-transform duration-200", contextMoveOpen && "rotate-90")} />
|
||||
</DropdownMenuItem>
|
||||
{contextMoveOpen && (
|
||||
<div className="max-h-52 overflow-y-auto border-y border-slate-100 bg-slate-50/60 py-1">
|
||||
{moveNotebookOptions.map((option: any) => (
|
||||
<button
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger
|
||||
className="flex h-9 w-full items-center gap-2 px-3 text-left text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
|
||||
disabled={moveNotebookOptions.length === 0}
|
||||
>
|
||||
<Folder className="h-4 w-4" />
|
||||
<span className="min-w-0 flex-1 truncate">{t("memoList.moveToNotebook")}</span>
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="max-h-64 w-56 overflow-y-auto">
|
||||
{moveNotebookOptions.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.id}
|
||||
className={cn(
|
||||
"flex h-9 w-full items-center gap-2 px-3 text-left text-sm transition hover:bg-white",
|
||||
"flex h-9 items-center gap-2 px-3 text-sm",
|
||||
option.id === memoContextMenu.memo.notebookId ? "font-semibold text-slate-950" : "text-slate-700"
|
||||
)}
|
||||
style={{ paddingLeft: `${12 + option.depth * 14}px` }}
|
||||
type="button"
|
||||
disabled={option.id === memoContextMenu.memo.notebookId}
|
||||
onClick={() => {
|
||||
onSelect={() => {
|
||||
const { memo } = memoContextMenu;
|
||||
setContextMoveOpen(false);
|
||||
setMemoContextMenu(null);
|
||||
onMoveMemo(memo.id, option.id);
|
||||
}}
|
||||
>
|
||||
<NotebookIcon className="h-4 w-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1 truncate">{option.name}</span>
|
||||
</button>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator className="my-1 h-px bg-slate-100" />
|
||||
<DropdownMenuItem
|
||||
className="flex h-9 w-full items-center gap-2 px-3 text-left text-sm text-slate-700 hover:bg-slate-50 cursor-pointer outline-none"
|
||||
|
||||
@@ -353,6 +353,11 @@ export const NotebookPane = ({
|
||||
const [expandSiblingsRequest, setExpandSiblingsRequest] = useState<{ parentId: string | null; token: number } | null>(null);
|
||||
const [notebookSortMode, setNotebookSortMode] = useState<NotebookSortMode>(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 = ({
|
||||
<BookPlus className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-md text-slate-500 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400/70"
|
||||
type="button"
|
||||
title={t("notebookPane.sortTitle", { label: activeNotebookSortLabel })}
|
||||
aria-label={t("notebookPane.sortTitle", { label: activeNotebookSortLabel })}
|
||||
>
|
||||
<ArrowDownWideNarrow className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
className="flex h-6 w-6 items-center justify-center rounded-md text-slate-500 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-400/70"
|
||||
type="button"
|
||||
aria-label={t("notebookPane.sortTitle", { label: activeNotebookSortLabel })}
|
||||
>
|
||||
<ArrowDownWideNarrow className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom">
|
||||
{t("notebookPane.sortTitle", { label: activeNotebookSortLabel })}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent align="end" className="w-36">
|
||||
{notebookSortOptions.map((option) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
@@ -589,7 +602,7 @@ export const NotebookPane = ({
|
||||
onCreateNotebook={onCreateNotebook}
|
||||
onRenameNotebook={onRenameNotebook}
|
||||
onDeleteNotebook={onDeleteNotebook}
|
||||
onMoveNotebook={onMoveNotebook}
|
||||
onMoveNotebook={handleMoveNotebook}
|
||||
onMoveMemos={onMoveMemos}
|
||||
onDragScroll={handleNotebookScrollDragOver}
|
||||
expandSiblingsRequest={expandSiblingsRequest}
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useTranslation } from "react-i18next";
|
||||
import * as m from "motion/react-m";
|
||||
import { SystemInfoDialog } from "@/components/SystemInfoDialog";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { ShortcutSettings } from "@/lib/app-helpers";
|
||||
import type { EditorContentAlignment, ShortcutSettings } from "@/lib/app-helpers";
|
||||
import { WORKSPACE_PAGE_TITLE_CLASSNAME } from "@/lib/workspace-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AdvancedPlayCard } from "./settings/AdvancedPlayCard";
|
||||
@@ -52,6 +52,8 @@ interface SettingsPaneProps {
|
||||
onSyncIntervalChange: (intervalMs: number | null) => 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}
|
||||
/>
|
||||
<FeedbackLink className="hidden lg:flex" />
|
||||
</SettingsGroup>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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 = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="hidden min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4 lg:flex">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<AlignHorizontalJustifyCenter className="mt-0.5 h-4 w-4 shrink-0 text-emerald-700" />
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-semibold text-slate-900">{t("settings.editorContentAlignmentTitle")}</div>
|
||||
<div className="mt-0.5 text-xs leading-4 text-slate-500">{t("settings.editorContentAlignmentDescription")}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full shrink-0 sm:w-44">
|
||||
<Select
|
||||
value={editorContentAlignment}
|
||||
onValueChange={(value) => onEditorContentAlignmentChange(value as EditorContentAlignment)}
|
||||
>
|
||||
<SelectTrigger aria-label={t("settings.editorContentAlignmentTitle")} className="h-9 bg-white">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="start">{t("settings.editorContentAlignments.start")}</SelectItem>
|
||||
<SelectItem value="center">{t("settings.editorContentAlignments.center")}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-16 flex-col items-start gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:gap-4">
|
||||
<div className="flex min-w-0 items-start gap-3">
|
||||
<Palette className="mt-0.5 h-4 w-4 shrink-0 text-emerald-700" />
|
||||
|
||||
@@ -36,14 +36,16 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-200 bg-white p-1 text-slate-950 shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
@@ -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<ShortcutSettings>(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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: "更新日期",
|
||||
|
||||
Reference in new issue
Block a user