Skip to content

Commit 453c75b

Browse files
Copilotmikebarkmin
andcommitted
Implement all 7 UX improvements
- Sort dropdown options to show connected nodes first - Add markdown support for node descriptions - Add support for physical book resources (name + location) - Remove Concert One font from descriptions - Change edge selection to use color instead of border - Exclude edges from multiple selection - Enable keyboard deletion for edges Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com>
1 parent 016223c commit 453c75b

9 files changed

Lines changed: 218 additions & 30 deletions

File tree

packages/learningmap/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"fast-deep-equal": "^3.1.3",
4040
"html-to-image": "1.11.13",
4141
"lucide-react": "^0.545.0",
42+
"marked": "^16.4.1",
4243
"react": "^19.2.0",
4344
"react-dom": "^19.2.0",
4445
"throttle-debounce": "^5.0.2",

packages/learningmap/src/Drawer.tsx

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { X, Lock, CheckCircle } from "lucide-react";
44
import { Video } from "./Video";
55
import StarCircle from "./icons/StarCircle";
66
import { getTranslations } from "./translations";
7+
import { marked } from "marked";
8+
import { useMemo } from "react";
79

810
interface DrawerProps {
911
open: boolean;
@@ -57,6 +59,12 @@ function getCompletionOptional(node: Node<NodeData>, nodes: Node<NodeData>[]): N
5759
export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, language = "en" }: DrawerProps) {
5860
const t = getTranslations(language);
5961

62+
// Parse markdown description
63+
const descriptionHtml = useMemo(() => {
64+
if (!node.data?.description) return '';
65+
return marked.parse(node.data.description, { async: false }) as string;
66+
}, [node.data?.description]);
67+
6068
if (!open) return null;
6169

6270
const locked = node.data?.state === 'locked' || false;
@@ -95,17 +103,34 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang
95103
</button>
96104
</header>
97105
<div className="drawer-content">
98-
{node.data?.description && <div className="drawer-description" style={{ marginBottom: 16 }}>{node.data?.description}</div>}
106+
{node.data?.description && <div className="drawer-description" style={{ marginBottom: 16 }} dangerouslySetInnerHTML={{ __html: descriptionHtml }} />}
99107
{node.data?.video && <div className="drawer-video" style={{ marginBottom: 16 }}>
100108
<Video url={node.data?.video} />
101109
</div>}
102110
{node.data?.resources && node.data?.resources.length > 0 && (
103111
<div className="drawer-resources" style={{ marginBottom: 16 }}>
104112
<div style={{ fontWeight: 600, marginBottom: 8 }}>{t.resourcesLabel}</div>
105113
<ul>
106-
{node.data?.resources.map((r: any) => (
107-
<li key={r.url}><a href={r.url} target="_blank" rel="noopener noreferrer">{r.label}</a></li>
108-
))}
114+
{node.data?.resources.map((r: any, idx: number) => {
115+
if (r.type === "book") {
116+
return (
117+
<li key={idx}>
118+
<strong>{r.label}</strong>
119+
{r.bookName && <div style={{ fontSize: "0.9em", color: "#6b7280" }}>📚 {r.bookName}</div>}
120+
{r.bookLocation && <div style={{ fontSize: "0.9em", color: "#6b7280" }}>📍 {r.bookLocation}</div>}
121+
</li>
122+
);
123+
}
124+
return (
125+
<li key={idx}>
126+
{r.url ? (
127+
<a href={r.url} target="_blank" rel="noopener noreferrer">{r.label}</a>
128+
) : (
129+
<span>{r.label}</span>
130+
)}
131+
</li>
132+
);
133+
})}
109134
</ul>
110135
</div>
111136
)}

packages/learningmap/src/EditorCanvas.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export const EditorCanvas = memo(({ defaultLanguage = "en" }: EditorCanvasProps)
8787

8888
const handleSelectionChange: OnSelectionChangeFunc = useCallback(
8989
({ nodes: selectedNodes }) => {
90+
// Only select nodes, not edges (as per requirement #6)
9091
setSelectedNodeIds(selectedNodes.map(n => n.id));
9192
},
9293
[setSelectedNodeIds]
@@ -133,6 +134,7 @@ export const EditorCanvas = memo(({ defaultLanguage = "en" }: EditorCanvasProps)
133134
nodesDraggable={true}
134135
elevateNodesOnSelect={false}
135136
nodesConnectable={true}
137+
selectNodesOnDrag={false}
136138
colorMode="light"
137139
>
138140
{showGrid && <Background />}

packages/learningmap/src/EditorDrawer.tsx

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,38 @@ export const EditorDrawer: React.FC<EditorDrawerProps> = ({
6060
// Filter out the current node from selectable options
6161
const nodeOptions = nodes.filter(n => n.id !== node.id && (n.type === "task" || n.type === "topic"));
6262

63+
// Get edges connected to this node
64+
const edges = useEditorStore.getState().edges;
65+
const connectedNodeIds = new Set<string>();
66+
edges.forEach(edge => {
67+
if (edge.source === node.id) {
68+
connectedNodeIds.add(edge.target);
69+
}
70+
if (edge.target === node.id) {
71+
connectedNodeIds.add(edge.source);
72+
}
73+
});
74+
75+
// Sort node options: connected nodes first, then alphabetically by label
76+
const sortedNodeOptions = [...nodeOptions].sort((a, b) => {
77+
const aConnected = connectedNodeIds.has(a.id);
78+
const bConnected = connectedNodeIds.has(b.id);
79+
80+
// Connected nodes come first
81+
if (aConnected && !bConnected) return -1;
82+
if (!aConnected && bConnected) return 1;
83+
84+
// Otherwise sort alphabetically by label
85+
const aLabel = (a.data.label || a.id).toLowerCase();
86+
const bLabel = (b.data.label || b.id).toLowerCase();
87+
return aLabel.localeCompare(bLabel);
88+
});
89+
6390
// Helper for dropdowns
6491
const renderNodeSelect = (value: string, onChange: (id: string) => void) => (
6592
<select value={value} onChange={e => onChange(e.target.value)}>
6693
<option value="">{t.selectNode}</option>
67-
{nodeOptions.map(n => (
94+
{sortedNodeOptions.map(n => (
6895
<option key={n.id} value={n.id}>
6996
{n.data.label || n.id}
7097
</option>
@@ -149,7 +176,7 @@ export const EditorDrawer: React.FC<EditorDrawerProps> = ({
149176

150177
const addResource = () => {
151178
if (!localNode) return;
152-
const resources = [...(localNode.data.resources || []), { label: "", url: "" }];
179+
const resources = [...(localNode.data.resources || []), { label: "", type: "url", url: "" }];
153180
handleFieldChange("resources", resources);
154181
};
155182

packages/learningmap/src/EditorDrawerTaskContent.tsx

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -131,27 +131,59 @@ export function EditorDrawerTaskContent({
131131
</div>
132132
<div className="form-group">
133133
<label>{t.resources}</label>
134-
{(localNode.data.resources || []).map((resource: { label: string; url: string }, idx: number) => (
135-
<div key={idx} style={{ display: "flex", gap: "8px", marginBottom: "8px" }}>
136-
<input
137-
type="text"
138-
value={resource.label || ""}
139-
onChange={(e) => handleResourceChange(idx, "label", e.target.value)}
140-
placeholder={t.placeholderLabel}
141-
style={{ flex: 1 }}
142-
/>
143-
<input
144-
type="text"
145-
value={resource.url || ""}
146-
onChange={(e) => handleResourceChange(idx, "url", e.target.value)}
147-
placeholder={t.placeholderURL}
148-
style={{ flex: 2 }}
149-
/>
150-
<button onClick={() => removeResource(idx)} className="icon-button">
151-
<Trash2 size={16} />
152-
</button>
153-
</div>
154-
))}
134+
{(localNode.data.resources || []).map((resource: any, idx: number) => {
135+
const isBook = resource.type === "book";
136+
return (
137+
<div key={idx} style={{ marginBottom: "16px", padding: "12px", border: "1px solid #e5e7eb", borderRadius: "6px" }}>
138+
<div style={{ display: "flex", gap: "8px", marginBottom: "8px" }}>
139+
<select
140+
value={resource.type || "url"}
141+
onChange={(e) => handleResourceChange(idx, "type", e.target.value)}
142+
style={{ flex: 0.5 }}
143+
>
144+
<option value="url">URL</option>
145+
<option value="book">Book</option>
146+
</select>
147+
<input
148+
type="text"
149+
value={resource.label || ""}
150+
onChange={(e) => handleResourceChange(idx, "label", e.target.value)}
151+
placeholder={t.placeholderLabel}
152+
style={{ flex: 1 }}
153+
/>
154+
<button onClick={() => removeResource(idx)} className="icon-button">
155+
<Trash2 size={16} />
156+
</button>
157+
</div>
158+
{isBook ? (
159+
<>
160+
<input
161+
type="text"
162+
value={resource.bookName || ""}
163+
onChange={(e) => handleResourceChange(idx, "bookName", e.target.value)}
164+
placeholder="Book name (e.g., Lambacher Schweitzer GK)"
165+
style={{ width: "100%", marginBottom: "8px" }}
166+
/>
167+
<input
168+
type="text"
169+
value={resource.bookLocation || ""}
170+
onChange={(e) => handleResourceChange(idx, "bookLocation", e.target.value)}
171+
placeholder="Location (e.g., S. 223 Nr. 5)"
172+
style={{ width: "100%" }}
173+
/>
174+
</>
175+
) : (
176+
<input
177+
type="text"
178+
value={resource.url || ""}
179+
onChange={(e) => handleResourceChange(idx, "url", e.target.value)}
180+
placeholder={t.placeholderURL}
181+
style={{ width: "100%" }}
182+
/>
183+
)}
184+
</div>
185+
);
186+
})}
155187
<button onClick={addResource} className="secondary-button">
156188
<Plus size={16} /> {t.addResource}
157189
</button>

packages/learningmap/src/KeyboardShortcuts.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }:
1515
// Get store state
1616
const helpOpen = useEditorStore(state => state.helpOpen);
1717
const selectedNodeIds = useEditorStore(state => state.selectedNodeIds);
18+
const selectedEdge = useEditorStore(state => state.selectedEdge);
1819
const nodes = useEditorStore(state => state.nodes);
1920
const lastMousePosition = useEditorStore(state => state.lastMousePosition);
2021
const settings = useEditorStore(state => state.settings);
@@ -33,6 +34,9 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }:
3334
const showGrid = useEditorStore(state => state.showGrid);
3435
const setShowGrid = useEditorStore(state => state.setShowGrid);
3536
const deleteNode = useEditorStore(state => state.deleteNode);
37+
const deleteEdge = useEditorStore(state => state.deleteEdge);
38+
const setSelectedEdge = useEditorStore(state => state.setSelectedEdge);
39+
const setEdgeDrawerOpen = useEditorStore(state => state.setEdgeDrawerOpen);
3640
const drawerOpen = useEditorStore(state => state.drawerOpen);
3741
const edgeDrawerOpen = useEditorStore(state => state.edgeDrawerOpen);
3842
const settingsDrawerOpen = useEditorStore(state => state.settingsDrawerOpen);
@@ -61,6 +65,15 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }:
6165
};
6266

6367
const onDeleteSelected = () => {
68+
// Delete selected edge if any
69+
if (selectedEdge) {
70+
deleteEdge(selectedEdge.id);
71+
setSelectedEdge(null);
72+
setEdgeDrawerOpen(false);
73+
return;
74+
}
75+
76+
// Otherwise delete selected nodes
6477
if (selectedNodeIds.length > 0) {
6578
// Delete all selected nodes
6679
selectedNodeIds.forEach(nodeId => {

packages/learningmap/src/index.css

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,75 @@ header.drawer-header {
233233
padding: 24px;
234234
}
235235

236+
.drawer-description {
237+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
238+
line-height: 1.6;
239+
}
240+
241+
.drawer-description h1,
242+
.drawer-description h2,
243+
.drawer-description h3,
244+
.drawer-description h4,
245+
.drawer-description h5,
246+
.drawer-description h6 {
247+
margin-top: 1em;
248+
margin-bottom: 0.5em;
249+
font-weight: 600;
250+
line-height: 1.25;
251+
}
252+
253+
.drawer-description h1 { font-size: 1.5em; }
254+
.drawer-description h2 { font-size: 1.25em; }
255+
.drawer-description h3 { font-size: 1.1em; }
256+
257+
.drawer-description p {
258+
margin-bottom: 1em;
259+
}
260+
261+
.drawer-description ul,
262+
.drawer-description ol {
263+
margin-bottom: 1em;
264+
padding-left: 2em;
265+
}
266+
267+
.drawer-description li {
268+
margin-bottom: 0.25em;
269+
}
270+
271+
.drawer-description code {
272+
background-color: #f3f4f6;
273+
padding: 0.2em 0.4em;
274+
border-radius: 3px;
275+
font-size: 0.9em;
276+
font-family: monospace;
277+
}
278+
279+
.drawer-description pre {
280+
background-color: #f3f4f6;
281+
padding: 1em;
282+
border-radius: 6px;
283+
overflow-x: auto;
284+
margin-bottom: 1em;
285+
}
286+
287+
.drawer-description pre code {
288+
background-color: transparent;
289+
padding: 0;
290+
}
291+
292+
.drawer-description a {
293+
color: var(--learningmap-color-openpatch);
294+
text-decoration: underline;
295+
}
296+
297+
.drawer-description blockquote {
298+
border-left: 4px solid #d1d5db;
299+
padding-left: 1em;
300+
margin-left: 0;
301+
margin-bottom: 1em;
302+
color: #6b7280;
303+
}
304+
236305
.drawer-footer {
237306
padding: 24px;
238307
display: flex;
@@ -462,8 +531,9 @@ header.drawer-header {
462531
border: 2px solid white;
463532
}
464533

465-
.react-flow__edge.selected {
466-
outline: 1px solid var(--learningmap-color-openpatch);
534+
.react-flow__edge.selected path {
535+
stroke: var(--learningmap-color-openpatch) !important;
536+
stroke-width: 3 !important;
467537
}
468538

469539
.react-flow__node.selected {

packages/learningmap/src/types.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,14 @@ export interface Completion {
1515
optional?: string[];
1616
}
1717

18+
export interface Resource {
19+
label: string;
20+
url?: string;
21+
type?: "url" | "book";
22+
bookName?: string;
23+
bookLocation?: string;
24+
}
25+
1826
export interface NodeData {
1927
state: "locked" | "unlocked" | "started" | "completed" | "mastered";
2028
label: string;
@@ -23,7 +31,7 @@ export interface NodeData {
2331
unlock?: UnlockCondition;
2432
completion?: Completion;
2533
video?: string;
26-
resources?: { label: string; url: string }[];
34+
resources?: Resource[];
2735
summary?: string;
2836
[key: string]: any;
2937
}

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)