Skip to content

Commit 39e3b10

Browse files
authored
Merge pull request #16 from openpatch/copilot/fix-keyboard-shortcut-issues
Fix keyboard shortcuts issues: scrolling, hints, cursor placement, and select all
2 parents 76fe607 + 5bcebd4 commit 39e3b10

File tree

4 files changed

+122
-34
lines changed

4 files changed

+122
-34
lines changed

packages/learningmap/src/EditorToolbar.tsx

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import { Menu, MenuButton, MenuDivider, MenuItem, SubMenu } from "@szhsin/react-menu";
33
import "@szhsin/react-menu/dist/index.css";
44
import '@szhsin/react-menu/dist/transitions/zoom.css';
5-
import { Plus, Bug, Settings, Eye, Menu as MenuI, FolderOpen, Download, ImageDown, ExternalLink, Share2 } from "lucide-react";
5+
import { Plus, Bug, Settings, Eye, Menu as MenuI, FolderOpen, Download, ImageDown, ExternalLink, Share2, RotateCcw } from "lucide-react";
66
import { getTranslations } from "./translations";
77

88
interface EditorToolbarProps {
@@ -21,6 +21,7 @@ interface EditorToolbarProps {
2121
onDownlad: () => void;
2222
onOpen: () => void;
2323
onShare: () => void;
24+
onReset: () => void;
2425
language?: string;
2526
}
2627

@@ -40,6 +41,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
4041
onDownlad,
4142
onOpen,
4243
onShare,
44+
onReset,
4345
language = "en",
4446
}) => {
4547
const t = getTranslations(language);
@@ -48,10 +50,22 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
4850
<div className="editor-toolbar">
4951
<div className="toolbar-group">
5052
<Menu menuButton={<MenuButton disabled={previewMode} className="toolbar-button"><Plus size={16} /> <span className="toolbar-label">{t.nodes}</span></MenuButton>}>
51-
<MenuItem onClick={() => onAddNewNode("task")}>{t.addTask}</MenuItem>
52-
<MenuItem onClick={() => onAddNewNode("topic")}>{t.addTopic}</MenuItem>
53-
<MenuItem onClick={() => onAddNewNode("image")}>{t.addImage}</MenuItem>
54-
<MenuItem onClick={() => onAddNewNode("text")}>{t.addText}</MenuItem>
53+
<MenuItem onClick={() => onAddNewNode("task")}>
54+
<span>{t.addTask}</span>
55+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+1</span>
56+
</MenuItem>
57+
<MenuItem onClick={() => onAddNewNode("topic")}>
58+
<span>{t.addTopic}</span>
59+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+2</span>
60+
</MenuItem>
61+
<MenuItem onClick={() => onAddNewNode("image")}>
62+
<span>{t.addImage}</span>
63+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+3</span>
64+
</MenuItem>
65+
<MenuItem onClick={() => onAddNewNode("text")}>
66+
<span>{t.addText}</span>
67+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+4</span>
68+
</MenuItem>
5569
</Menu>
5670
<button disabled={previewMode} onClick={onOpenSettingsDrawer} className="toolbar-button">
5771
<Settings size={16} /> <span className="toolbar-label">{t.settings}</span>
@@ -69,9 +83,15 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
6983
<Share2 size={16} /> <span>{t.share}</span>
7084
</MenuItem>
7185
<MenuDivider />
86+
<MenuItem onClick={onReset}>
87+
<RotateCcw size={16} /> <span>{t.reset}</span>
88+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+Del</span>
89+
</MenuItem>
90+
<MenuDivider />
7291
<SubMenu className={`${debugMode ? "active" : ""}`} label={<><Bug size={16} /> <span>{t.debug}</span></>}>
7392
<MenuItem type="checkbox" checked={debugMode} onClick={onToggleDebugMode}>
74-
{t.enableDebugMode}
93+
<span>{t.enableDebugMode}</span>
94+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+D</span>
7595
</MenuItem>
7696
<MenuItem type="checkbox" checked={showCompletionNeeds} onClick={e => onSetShowCompletionNeeds(e.checked ?? false)} disabled={!debugMode}>
7797
{t.showCompletionNeedsEdges}
@@ -85,6 +105,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
85105
</SubMenu>
86106
<MenuItem onClick={onTogglePreviewMode} className={`${previewMode ? "active" : ""}`}>
87107
<Eye size={16} /> <span>{t.preview}</span>
108+
<span style={{ marginLeft: 'auto', paddingLeft: '16px', color: '#9ca3af', fontSize: '0.875rem' }}>Ctrl+P</span>
88109
</MenuItem>
89110
<MenuDivider />
90111
<MenuItem href="https://openpatch.org" target="_blank" rel="noopener noreferrer">

packages/learningmap/src/LearningMapEditor.tsx

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import FloatingEdge from "./FloatingEdge";
3030
import { EditorToolbar } from "./EditorToolbar";
3131
import { parseRoadmapData } from "./helper";
3232
import { LearningMap } from "./LearningMap";
33-
import { Info, Redo, Undo, RotateCw, ShieldAlert } from "lucide-react";
33+
import { Info, Redo, Undo, RotateCw, ShieldAlert, X } from "lucide-react";
3434
import useUndoable from "./useUndoable";
3535
import { MultiNodePanel } from "./MultiNodePanel";
3636
import { getTranslations } from "./translations";
@@ -82,6 +82,7 @@ export function LearningMapEditor({
8282
const [settings, setSettings] = useState<Settings>(parsedRoadmap.settings);
8383
const [showGrid, setShowGrid] = useState(false);
8484
const [clipboard, setClipboard] = useState<{ nodes: Node<NodeData>[]; edges: Edge[] } | null>(null);
85+
const [lastMousePosition, setLastMousePosition] = useState<{ x: number; y: number } | null>(null);
8586

8687
// Use language from settings if available, otherwise use prop
8788
const effectiveLanguage = settings?.language || language;
@@ -98,6 +99,7 @@ export function LearningMapEditor({
9899
{ action: t.shortcuts.togglePreviewMode, shortcut: "Ctrl+P" },
99100
{ action: t.shortcuts.toggleDebugMode, shortcut: "Ctrl+D" },
100101
{ action: t.shortcuts.selectMultipleNodes, shortcut: "Ctrl+Click or Shift+Drag" },
102+
{ action: t.shortcuts.selectAllNodes, shortcut: "Ctrl+A" },
101103
{ action: t.shortcuts.showHelp, shortcut: "Ctrl+? or Help Button" },
102104
{ action: t.shortcuts.zoomIn, shortcut: "Ctrl++" },
103105
{ action: t.shortcuts.zoomOut, shortcut: "Ctrl+-" },
@@ -306,12 +308,16 @@ export function LearningMapEditor({
306308

307309
const addNewNode = useCallback(
308310
(type: "task" | "topic" | "image" | "text") => {
309-
const centerPos = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
311+
// Use last mouse position if available, otherwise use center of screen
312+
const position = lastMousePosition
313+
? screenToFlowPosition(lastMousePosition)
314+
: screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
315+
310316
if (type === "task") {
311317
const newNode: Node<NodeData> = {
312318
id: `node${nextNodeId}`,
313319
type,
314-
position: centerPos,
320+
position,
315321
data: {
316322
label: t.newTask,
317323
summary: "",
@@ -324,7 +330,7 @@ export function LearningMapEditor({
324330
const newNode: Node<NodeData> = {
325331
id: `node${nextNodeId}`,
326332
type,
327-
position: centerPos,
333+
position,
328334
data: {
329335
label: t.newTopic,
330336
summary: "",
@@ -339,7 +345,7 @@ export function LearningMapEditor({
339345
id: `background-node${nextNodeId}`,
340346
type,
341347
zIndex: -2,
342-
position: centerPos,
348+
position,
343349
data: {
344350
src: "",
345351
},
@@ -350,7 +356,7 @@ export function LearningMapEditor({
350356
const newNode: Node<TextNodeData> = {
351357
id: `background-node${nextNodeId}`,
352358
type,
353-
position: centerPos,
359+
position,
354360
zIndex: -1,
355361
data: {
356362
text: t.backgroundTextDefault,
@@ -363,7 +369,7 @@ export function LearningMapEditor({
363369
}
364370
setSaved(false);
365371
},
366-
[nextNodeId, screenToFlowPosition, setNodes, setSaved, t]
372+
[nextNodeId, lastMousePosition, screenToFlowPosition, setNodes, setSaved, t]
367373
);
368374

369375
const handleSave = useCallback(() => {
@@ -679,13 +685,31 @@ export function LearningMapEditor({
679685
}
680686
}, [setNodes, setEdges, setNextNodeId, setSaved, t]);
681687

688+
const handleSelectAll = useCallback(() => {
689+
setNodes(nds => nds.map(n => ({
690+
...n,
691+
selected: true,
692+
})))
693+
}, [nodes, setSelectedNodeIds]);
694+
682695
const handleSelectionChange: OnSelectionChangeFunc = useCallback(
683696
({ nodes: selectedNodes }) => {
684697
setSelectedNodeIds(selectedNodes.map(n => n.id));
685698
},
686699
[setSelectedNodeIds]
687700
);
688701

702+
// Track mouse position for node placement
703+
useEffect(() => {
704+
const handleMouseMove = (e: MouseEvent) => {
705+
setLastMousePosition({ x: e.clientX, y: e.clientY });
706+
};
707+
window.addEventListener("mousemove", handleMouseMove);
708+
return () => {
709+
window.removeEventListener("mousemove", handleMouseMove);
710+
};
711+
}, []);
712+
689713
useEffect(() => {
690714
const handleKeyDown = (e: KeyboardEvent) => {
691715
//save shortcut
@@ -764,7 +788,6 @@ export function LearningMapEditor({
764788
handleZoomToSelection();
765789
}
766790

767-
console.log(e);
768791
// Toggle grid shortcut
769792
if ((e.ctrlKey || e.metaKey) && e.code === "Backslash") {
770793
e.preventDefault();
@@ -790,6 +813,11 @@ export function LearningMapEditor({
790813
e.preventDefault();
791814
handlePaste();
792815
}
816+
// Select all shortcut
817+
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'a' && !e.shiftKey) {
818+
e.preventDefault();
819+
handleSelectAll();
820+
}
793821

794822
// Dismiss with Escape
795823
if (helpOpen && e.key === 'Escape') {
@@ -802,7 +830,7 @@ export function LearningMapEditor({
802830
};
803831
}, [handleSave, handleUndo, handleRedo, addNewNode, helpOpen, setHelpOpen, togglePreviewMode, toggleDebugMode,
804832
handleZoomIn, handleZoomOut, handleResetZoom, handleFitView, handleZoomToSelection, handleToggleGrid,
805-
handleResetMap, handleCut, handleCopy, handlePaste]);
833+
handleResetMap, handleCut, handleCopy, handlePaste, handleSelectAll]);
806834

807835
return (
808836
<>
@@ -822,6 +850,7 @@ export function LearningMapEditor({
822850
onDownlad={handleDownload}
823851
onOpen={handleOpen}
824852
onShare={handleShare}
853+
onReset={handleResetMap}
825854
language={effectiveLanguage}
826855
/>
827856
{previewMode && <LearningMap roadmapData={roadmapState} language={effectiveLanguage} />}
@@ -907,24 +936,33 @@ export function LearningMapEditor({
907936
open={helpOpen}
908937
onClose={() => setHelpOpen(false)}
909938
>
910-
<h2>{t.keyboardShortcuts}</h2>
911-
<table>
912-
<thead>
913-
<tr>
914-
<th>{t.action}</th>
915-
<th>{t.shortcut}</th>
916-
</tr>
917-
</thead>
918-
<tbody>
919-
{keyboardShortcuts.map((item) => (
920-
<tr key={item.action}>
921-
<td>{item.action}</td>
922-
<td>{item.shortcut}</td>
939+
<header className="help-header">
940+
<h2>{t.keyboardShortcuts}</h2>
941+
<button className="close-button" onClick={() => setHelpOpen(false)} aria-label={t.close}>
942+
<X size={20} />
943+
</button>
944+
</header>
945+
<div className="help-content">
946+
<table>
947+
<thead>
948+
<tr>
949+
<th>{t.action}</th>
950+
<th>{t.shortcut}</th>
923951
</tr>
924-
))}
925-
</tbody>
926-
</table>
927-
<button className="primary-button" onClick={() => setHelpOpen(false)}>{t.close}</button>
952+
</thead>
953+
<tbody>
954+
{keyboardShortcuts.map((item) => (
955+
<tr key={item.action}>
956+
<td>{item.action}</td>
957+
<td>{item.shortcut}</td>
958+
</tr>
959+
))}
960+
</tbody>
961+
</table>
962+
</div>
963+
<div className="help-footer">
964+
<button className="primary-button" onClick={() => setHelpOpen(false)}>{t.close}</button>
965+
</div>
928966
</dialog>
929967
<ShareDialog
930968
open={shareDialogOpen}

packages/learningmap/src/index.css

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -624,18 +624,44 @@ header.drawer-header {
624624
dialog.help[open] {
625625
width: 600px;
626626
max-width: 90vw;
627+
max-height: 90vh;
627628
border: none;
628629
border-radius: 12px;
629630
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
630-
padding: 24px;
631+
padding: 0;
631632
background: white;
632633
position: fixed;
633634
top: 50%;
634635
transform: translateY(-50%);
635-
gap: 16px;
636636
display: flex;
637637
flex-direction: column;
638638
z-index: 1000;
639+
overflow: hidden;
640+
641+
.help-header {
642+
padding: 24px 24px 16px 24px;
643+
display: flex;
644+
justify-content: space-between;
645+
align-items: center;
646+
border-bottom: 1px solid #e5e7eb;
647+
648+
h2 {
649+
margin: 0;
650+
font-size: 24px;
651+
font-weight: 700;
652+
}
653+
}
654+
655+
.help-content {
656+
flex: 1;
657+
overflow-y: auto;
658+
padding: 16px 24px;
659+
}
660+
661+
.help-footer {
662+
padding: 16px 24px 24px 24px;
663+
border-top: 1px solid #e5e7eb;
664+
}
639665

640666
table {
641667
width: 100%;

packages/learningmap/src/translations.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface Translations {
4343
togglePreviewMode: string;
4444
toggleDebugMode: string;
4545
selectMultipleNodes: string;
46+
selectAllNodes: string;
4647
showHelp: string;
4748
zoomIn: string;
4849
zoomOut: string;
@@ -234,6 +235,7 @@ const en: Translations = {
234235
togglePreviewMode: "Toggle Preview Mode",
235236
toggleDebugMode: "Toggle Debug Mode",
236237
selectMultipleNodes: "Select Multiple Nodes",
238+
selectAllNodes: "Select All Nodes",
237239
showHelp: "Show Help",
238240
zoomIn: "Zoom In",
239241
zoomOut: "Zoom Out",
@@ -430,6 +432,7 @@ const de: Translations = {
430432
togglePreviewMode: "Vorschau-Modus umschalten",
431433
toggleDebugMode: "Debug-Modus umschalten",
432434
selectMultipleNodes: "Mehrere Knoten auswählen",
435+
selectAllNodes: "Alle Knoten auswählen",
433436
showHelp: "Hilfe anzeigen",
434437
zoomIn: "Vergrößern",
435438
zoomOut: "Verkleinern",

0 commit comments

Comments
 (0)