mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: implement platform-aware keyboard shortcuts for sidebar and toolbar actions, enhancing user experience
This commit is contained in:
parent
baa02c50e4
commit
7b44dd58f5
6 changed files with 113 additions and 15 deletions
|
|
@ -1,8 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { MarkdownPlugin, remarkMdx } from "@platejs/markdown";
|
||||
import { Plate, usePlateEditor } from "platejs/react";
|
||||
import { createPlatePlugin, Key, Plate, usePlateEditor } from "platejs/react";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
|
||||
|
|
@ -69,6 +69,31 @@ export function PlateEditor({
|
|||
}: PlateEditorProps) {
|
||||
const lastMarkdownRef = useRef(markdown);
|
||||
|
||||
// Keep a stable ref to the latest onSave callback so the plugin shortcut
|
||||
// always calls the most recent version without re-creating the editor.
|
||||
const onSaveRef = useRef(onSave);
|
||||
useEffect(() => {
|
||||
onSaveRef.current = onSave;
|
||||
}, [onSave]);
|
||||
|
||||
// Stable Plate plugin for ⌘+S / Ctrl+S save shortcut
|
||||
const SaveShortcutPlugin = useMemo(
|
||||
() =>
|
||||
createPlatePlugin({
|
||||
key: "save-shortcut",
|
||||
shortcuts: {
|
||||
save: {
|
||||
keys: [[Key.Mod, "s"]],
|
||||
handler: () => {
|
||||
onSaveRef.current?.();
|
||||
},
|
||||
preventDefault: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
// When readOnly is forced, always start in readOnly.
|
||||
// Otherwise, respect defaultEditing to decide initial mode.
|
||||
// The user can still toggle between editing/viewing via ModeToolbarButton.
|
||||
|
|
@ -89,6 +114,7 @@ export function PlateEditor({
|
|||
...FloatingToolbarKit,
|
||||
...AutoformatKit,
|
||||
...DndKit,
|
||||
SaveShortcutPlugin,
|
||||
MarkdownPlugin.configure({
|
||||
options: {
|
||||
remarkPlugins: [remarkGfm, remarkMath, remarkMdx],
|
||||
|
|
|
|||
|
|
@ -40,10 +40,10 @@ export function useSidebarState(defaultCollapsed = false): UseSidebarStateReturn
|
|||
setIsCollapsed(!isCollapsed);
|
||||
}, [isCollapsed, setIsCollapsed]);
|
||||
|
||||
// Keyboard shortcut: Cmd/Ctrl + B
|
||||
// Keyboard shortcut: Cmd/Ctrl + \
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "b" && (event.metaKey || event.ctrlKey)) {
|
||||
if (event.key === "\\" && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
toggleCollapsed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { PanelLeft, PanelLeftClose } from "lucide-react";
|
|||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { usePlatformShortcut } from "@/hooks/use-platform-shortcut";
|
||||
|
||||
interface SidebarCollapseButtonProps {
|
||||
isCollapsed: boolean;
|
||||
|
|
@ -17,6 +18,7 @@ export function SidebarCollapseButton({
|
|||
disableTooltip = false,
|
||||
}: SidebarCollapseButtonProps) {
|
||||
const t = useTranslations("sidebar");
|
||||
const { shortcut } = usePlatformShortcut();
|
||||
|
||||
const button = (
|
||||
<Button variant="ghost" size="icon" onClick={onToggle} className="h-8 w-8 shrink-0">
|
||||
|
|
@ -33,7 +35,9 @@ export function SidebarCollapseButton({
|
|||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent side={isCollapsed ? "right" : "bottom"}>
|
||||
{isCollapsed ? `${t("expand_sidebar")} (⌘B)` : `${t("collapse_sidebar")} (⌘B)`}
|
||||
{isCollapsed
|
||||
? `${t("expand_sidebar")} ${shortcut("Mod", "\\")}`
|
||||
: `${t("collapse_sidebar")} ${shortcut("Mod", "\\")}`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { KEYS } from "platejs";
|
|||
import { useEditorReadOnly, useEditorRef } from "platejs/react";
|
||||
|
||||
import { useEditorSave } from "@/components/editor/editor-save-context";
|
||||
import { usePlatformShortcut } from "@/hooks/use-platform-shortcut";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
|
||||
import { InsertToolbarButton } from "./insert-toolbar-button";
|
||||
|
|
@ -30,6 +31,7 @@ export function FixedToolbarButtons() {
|
|||
const readOnly = useEditorReadOnly();
|
||||
const editor = useEditorRef();
|
||||
const { onSave, hasUnsavedChanges, isSaving, canToggleMode } = useEditorSave();
|
||||
const { shortcut } = usePlatformShortcut();
|
||||
|
||||
return (
|
||||
<div className="flex w-full items-center">
|
||||
|
|
@ -39,7 +41,7 @@ export function FixedToolbarButtons() {
|
|||
<>
|
||||
<ToolbarGroup>
|
||||
<ToolbarButton
|
||||
tooltip="Undo (⌘+Z)"
|
||||
tooltip={`Undo ${shortcut("Mod", "Z")}`}
|
||||
onClick={() => {
|
||||
editor.undo();
|
||||
editor.tf.focus();
|
||||
|
|
@ -49,7 +51,7 @@ export function FixedToolbarButtons() {
|
|||
</ToolbarButton>
|
||||
|
||||
<ToolbarButton
|
||||
tooltip="Redo (⌘+⇧+Z)"
|
||||
tooltip={`Redo ${shortcut("Mod", "Shift", "Z")}`}
|
||||
onClick={() => {
|
||||
editor.redo();
|
||||
editor.tf.focus();
|
||||
|
|
@ -65,27 +67,36 @@ export function FixedToolbarButtons() {
|
|||
</ToolbarGroup>
|
||||
|
||||
<ToolbarGroup>
|
||||
<MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
|
||||
<MarkToolbarButton nodeType={KEYS.bold} tooltip={`Bold ${shortcut("Mod", "B")}`}>
|
||||
<BoldIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.italic} tooltip="Italic (⌘+I)">
|
||||
<MarkToolbarButton nodeType={KEYS.italic} tooltip={`Italic ${shortcut("Mod", "I")}`}>
|
||||
<ItalicIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.underline} tooltip="Underline (⌘+U)">
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.underline}
|
||||
tooltip={`Underline ${shortcut("Mod", "U")}`}
|
||||
>
|
||||
<UnderlineIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.strikethrough} tooltip="Strikethrough (⌘+⇧+M)">
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.strikethrough}
|
||||
tooltip={`Strikethrough ${shortcut("Mod", "Shift", "X")}`}
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
|
||||
<MarkToolbarButton nodeType={KEYS.code} tooltip={`Code ${shortcut("Mod", "E")}`}>
|
||||
<Code2Icon />
|
||||
</MarkToolbarButton>
|
||||
|
||||
<MarkToolbarButton nodeType={KEYS.highlight} tooltip="Highlight (⌘+⇧+H)">
|
||||
<MarkToolbarButton
|
||||
nodeType={KEYS.highlight}
|
||||
tooltip={`Highlight ${shortcut("Mod", "Shift", "H")}`}
|
||||
>
|
||||
<HighlighterIcon />
|
||||
</MarkToolbarButton>
|
||||
</ToolbarGroup>
|
||||
|
|
@ -103,7 +114,7 @@ export function FixedToolbarButtons() {
|
|||
{!readOnly && onSave && hasUnsavedChanges && (
|
||||
<ToolbarGroup>
|
||||
<ToolbarButton
|
||||
tooltip={isSaving ? "Saving..." : "Save (⌘+S)"}
|
||||
tooltip={isSaving ? "Saving..." : `Save ${shortcut("Mod", "S")}`}
|
||||
onClick={onSave}
|
||||
disabled={isSaving}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
|||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "\\";
|
||||
|
||||
type SidebarContext = {
|
||||
state: "expanded" | "collapsed";
|
||||
|
|
|
|||
57
surfsense_web/hooks/use-platform-shortcut.ts
Normal file
57
surfsense_web/hooks/use-platform-shortcut.ts
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
interface NavigatorUAData {
|
||||
platform: string;
|
||||
}
|
||||
|
||||
function getIsMac() {
|
||||
if (typeof navigator === "undefined") return false;
|
||||
|
||||
// Modern API (Chromium browsers: Chrome, Edge, Opera)
|
||||
const uaData = (navigator as Navigator & { userAgentData?: NavigatorUAData }).userAgentData;
|
||||
if (uaData?.platform) {
|
||||
return uaData.platform === "macOS";
|
||||
}
|
||||
|
||||
// Fallback for Firefox/Safari
|
||||
return /Mac|iPhone/.test(navigator.platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a helper that formats keyboard shortcut strings with
|
||||
* platform-aware modifier symbols.
|
||||
*
|
||||
* SSR-safe: returns an empty string until mounted so there is no hydration
|
||||
* mismatch.
|
||||
*/
|
||||
export function usePlatformShortcut() {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [isMac, setIsMac] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsMac(getIsMac());
|
||||
setReady(true);
|
||||
}, []);
|
||||
|
||||
const shortcut = useCallback(
|
||||
(...keys: string[]) => {
|
||||
if (!ready) return "";
|
||||
|
||||
const mod = isMac ? "⌘" : "Ctrl";
|
||||
const shift = isMac ? "⇧" : "Shift";
|
||||
const alt = isMac ? "⌥" : "Alt";
|
||||
|
||||
const mapped = keys.map((k) => {
|
||||
if (k === "Mod") return mod;
|
||||
if (k === "Shift") return shift;
|
||||
if (k === "Alt") return alt;
|
||||
return k;
|
||||
});
|
||||
|
||||
return `(${mapped.join("+")})`;
|
||||
},
|
||||
[ready, isMac]
|
||||
);
|
||||
|
||||
return { shortcut, isMac, ready };
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue