From 016223cf44537459dfdfbafde084a48d6ba52cb8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:38:45 +0000 Subject: [PATCH 1/7] Initial plan From 453c75bb5dc44e0296769dab5df25c63e25b4971 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:49:43 +0000 Subject: [PATCH 2/7] 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> --- packages/learningmap/package.json | 1 + packages/learningmap/src/Drawer.tsx | 33 ++++++++- packages/learningmap/src/EditorCanvas.tsx | 2 + packages/learningmap/src/EditorDrawer.tsx | 31 +++++++- .../src/EditorDrawerTaskContent.tsx | 74 +++++++++++++------ .../learningmap/src/KeyboardShortcuts.tsx | 13 ++++ packages/learningmap/src/index.css | 74 ++++++++++++++++++- packages/learningmap/src/types.ts | 10 ++- pnpm-lock.yaml | 10 +++ 9 files changed, 218 insertions(+), 30 deletions(-) diff --git a/packages/learningmap/package.json b/packages/learningmap/package.json index 96e58ef..389651a 100644 --- a/packages/learningmap/package.json +++ b/packages/learningmap/package.json @@ -39,6 +39,7 @@ "fast-deep-equal": "^3.1.3", "html-to-image": "1.11.13", "lucide-react": "^0.545.0", + "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0", "throttle-debounce": "^5.0.2", diff --git a/packages/learningmap/src/Drawer.tsx b/packages/learningmap/src/Drawer.tsx index 204deed..4276662 100644 --- a/packages/learningmap/src/Drawer.tsx +++ b/packages/learningmap/src/Drawer.tsx @@ -4,6 +4,8 @@ import { X, Lock, CheckCircle } from "lucide-react"; import { Video } from "./Video"; import StarCircle from "./icons/StarCircle"; import { getTranslations } from "./translations"; +import { marked } from "marked"; +import { useMemo } from "react"; interface DrawerProps { open: boolean; @@ -57,6 +59,12 @@ function getCompletionOptional(node: Node, nodes: Node[]): N export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, language = "en" }: DrawerProps) { const t = getTranslations(language); + // Parse markdown description + const descriptionHtml = useMemo(() => { + if (!node.data?.description) return ''; + return marked.parse(node.data.description, { async: false }) as string; + }, [node.data?.description]); + if (!open) return null; const locked = node.data?.state === 'locked' || false; @@ -95,7 +103,7 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang
- {node.data?.description &&
{node.data?.description}
} + {node.data?.description &&
} {node.data?.video &&
} @@ -103,9 +111,26 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang
{t.resourcesLabel}
    - {node.data?.resources.map((r: any) => ( -
  • {r.label}
  • - ))} + {node.data?.resources.map((r: any, idx: number) => { + if (r.type === "book") { + return ( +
  • + {r.label} + {r.bookName &&
    📚 {r.bookName}
    } + {r.bookLocation &&
    📍 {r.bookLocation}
    } +
  • + ); + } + return ( +
  • + {r.url ? ( + {r.label} + ) : ( + {r.label} + )} +
  • + ); + })}
)} diff --git a/packages/learningmap/src/EditorCanvas.tsx b/packages/learningmap/src/EditorCanvas.tsx index f459a3a..136af1f 100644 --- a/packages/learningmap/src/EditorCanvas.tsx +++ b/packages/learningmap/src/EditorCanvas.tsx @@ -87,6 +87,7 @@ export const EditorCanvas = memo(({ defaultLanguage = "en" }: EditorCanvasProps) const handleSelectionChange: OnSelectionChangeFunc = useCallback( ({ nodes: selectedNodes }) => { + // Only select nodes, not edges (as per requirement #6) setSelectedNodeIds(selectedNodes.map(n => n.id)); }, [setSelectedNodeIds] @@ -133,6 +134,7 @@ export const EditorCanvas = memo(({ defaultLanguage = "en" }: EditorCanvasProps) nodesDraggable={true} elevateNodesOnSelect={false} nodesConnectable={true} + selectNodesOnDrag={false} colorMode="light" > {showGrid && } diff --git a/packages/learningmap/src/EditorDrawer.tsx b/packages/learningmap/src/EditorDrawer.tsx index 92d506d..660fbe7 100644 --- a/packages/learningmap/src/EditorDrawer.tsx +++ b/packages/learningmap/src/EditorDrawer.tsx @@ -60,11 +60,38 @@ export const EditorDrawer: React.FC = ({ // Filter out the current node from selectable options const nodeOptions = nodes.filter(n => n.id !== node.id && (n.type === "task" || n.type === "topic")); + // Get edges connected to this node + const edges = useEditorStore.getState().edges; + const connectedNodeIds = new Set(); + edges.forEach(edge => { + if (edge.source === node.id) { + connectedNodeIds.add(edge.target); + } + if (edge.target === node.id) { + connectedNodeIds.add(edge.source); + } + }); + + // Sort node options: connected nodes first, then alphabetically by label + const sortedNodeOptions = [...nodeOptions].sort((a, b) => { + const aConnected = connectedNodeIds.has(a.id); + const bConnected = connectedNodeIds.has(b.id); + + // Connected nodes come first + if (aConnected && !bConnected) return -1; + if (!aConnected && bConnected) return 1; + + // Otherwise sort alphabetically by label + const aLabel = (a.data.label || a.id).toLowerCase(); + const bLabel = (b.data.label || b.id).toLowerCase(); + return aLabel.localeCompare(bLabel); + }); + // Helper for dropdowns const renderNodeSelect = (value: string, onChange: (id: string) => void) => ( handleResourceChange(idx, "label", e.target.value)} - placeholder={t.placeholderLabel} - style={{ flex: 1 }} - /> - handleResourceChange(idx, "url", e.target.value)} - placeholder={t.placeholderURL} - style={{ flex: 2 }} - /> - -
- ))} + {(localNode.data.resources || []).map((resource: any, idx: number) => { + const isBook = resource.type === "book"; + return ( +
+
+ + handleResourceChange(idx, "label", e.target.value)} + placeholder={t.placeholderLabel} + style={{ flex: 1 }} + /> + +
+ {isBook ? ( + <> + handleResourceChange(idx, "bookName", e.target.value)} + placeholder="Book name (e.g., Lambacher Schweitzer GK)" + style={{ width: "100%", marginBottom: "8px" }} + /> + handleResourceChange(idx, "bookLocation", e.target.value)} + placeholder="Location (e.g., S. 223 Nr. 5)" + style={{ width: "100%" }} + /> + + ) : ( + handleResourceChange(idx, "url", e.target.value)} + placeholder={t.placeholderURL} + style={{ width: "100%" }} + /> + )} +
+ ); + })} diff --git a/packages/learningmap/src/KeyboardShortcuts.tsx b/packages/learningmap/src/KeyboardShortcuts.tsx index 2621f8b..9a7d41e 100644 --- a/packages/learningmap/src/KeyboardShortcuts.tsx +++ b/packages/learningmap/src/KeyboardShortcuts.tsx @@ -15,6 +15,7 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }: // Get store state const helpOpen = useEditorStore(state => state.helpOpen); const selectedNodeIds = useEditorStore(state => state.selectedNodeIds); + const selectedEdge = useEditorStore(state => state.selectedEdge); const nodes = useEditorStore(state => state.nodes); const lastMousePosition = useEditorStore(state => state.lastMousePosition); const settings = useEditorStore(state => state.settings); @@ -33,6 +34,9 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }: const showGrid = useEditorStore(state => state.showGrid); const setShowGrid = useEditorStore(state => state.setShowGrid); const deleteNode = useEditorStore(state => state.deleteNode); + const deleteEdge = useEditorStore(state => state.deleteEdge); + const setSelectedEdge = useEditorStore(state => state.setSelectedEdge); + const setEdgeDrawerOpen = useEditorStore(state => state.setEdgeDrawerOpen); const drawerOpen = useEditorStore(state => state.drawerOpen); const edgeDrawerOpen = useEditorStore(state => state.edgeDrawerOpen); const settingsDrawerOpen = useEditorStore(state => state.settingsDrawerOpen); @@ -61,6 +65,15 @@ export const KeyboardShortcuts = ({ jsonStore = "https://json.openpatch.org" }: }; const onDeleteSelected = () => { + // Delete selected edge if any + if (selectedEdge) { + deleteEdge(selectedEdge.id); + setSelectedEdge(null); + setEdgeDrawerOpen(false); + return; + } + + // Otherwise delete selected nodes if (selectedNodeIds.length > 0) { // Delete all selected nodes selectedNodeIds.forEach(nodeId => { diff --git a/packages/learningmap/src/index.css b/packages/learningmap/src/index.css index 3f3314a..bc2207b 100644 --- a/packages/learningmap/src/index.css +++ b/packages/learningmap/src/index.css @@ -233,6 +233,75 @@ header.drawer-header { padding: 24px; } +.drawer-description { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + line-height: 1.6; +} + +.drawer-description h1, +.drawer-description h2, +.drawer-description h3, +.drawer-description h4, +.drawer-description h5, +.drawer-description h6 { + margin-top: 1em; + margin-bottom: 0.5em; + font-weight: 600; + line-height: 1.25; +} + +.drawer-description h1 { font-size: 1.5em; } +.drawer-description h2 { font-size: 1.25em; } +.drawer-description h3 { font-size: 1.1em; } + +.drawer-description p { + margin-bottom: 1em; +} + +.drawer-description ul, +.drawer-description ol { + margin-bottom: 1em; + padding-left: 2em; +} + +.drawer-description li { + margin-bottom: 0.25em; +} + +.drawer-description code { + background-color: #f3f4f6; + padding: 0.2em 0.4em; + border-radius: 3px; + font-size: 0.9em; + font-family: monospace; +} + +.drawer-description pre { + background-color: #f3f4f6; + padding: 1em; + border-radius: 6px; + overflow-x: auto; + margin-bottom: 1em; +} + +.drawer-description pre code { + background-color: transparent; + padding: 0; +} + +.drawer-description a { + color: var(--learningmap-color-openpatch); + text-decoration: underline; +} + +.drawer-description blockquote { + border-left: 4px solid #d1d5db; + padding-left: 1em; + margin-left: 0; + margin-bottom: 1em; + color: #6b7280; +} + .drawer-footer { padding: 24px; display: flex; @@ -462,8 +531,9 @@ header.drawer-header { border: 2px solid white; } -.react-flow__edge.selected { - outline: 1px solid var(--learningmap-color-openpatch); +.react-flow__edge.selected path { + stroke: var(--learningmap-color-openpatch) !important; + stroke-width: 3 !important; } .react-flow__node.selected { diff --git a/packages/learningmap/src/types.ts b/packages/learningmap/src/types.ts index 0944b58..2558846 100644 --- a/packages/learningmap/src/types.ts +++ b/packages/learningmap/src/types.ts @@ -15,6 +15,14 @@ export interface Completion { optional?: string[]; } +export interface Resource { + label: string; + url?: string; + type?: "url" | "book"; + bookName?: string; + bookLocation?: string; +} + export interface NodeData { state: "locked" | "unlocked" | "started" | "completed" | "mastered"; label: string; @@ -23,7 +31,7 @@ export interface NodeData { unlock?: UnlockCondition; completion?: Completion; video?: string; - resources?: { label: string; url: string }[]; + resources?: Resource[]; summary?: string; [key: string]: any; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f281cff..c79d202 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: lucide-react: specifier: ^0.545.0 version: 0.545.0(react@19.2.0) + marked: + specifier: ^16.4.1 + version: 16.4.1 react: specifier: ^19.2.0 version: 19.2.0 @@ -2496,6 +2499,11 @@ packages: make-error@1.3.6: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + marked@16.4.1: + resolution: {integrity: sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==} + engines: {node: '>= 20'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -6087,6 +6095,8 @@ snapshots: make-error@1.3.6: {} + marked@16.4.1: {} + math-intrinsics@1.1.0: {} merge-stream@2.0.0: {} From 0ff29e7cc90f740a296eda6567e72ff36a6701eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 11:53:15 +0000 Subject: [PATCH 3/7] Address code review: Add type safety and XSS protection - Import and use Resource type instead of 'any' - Add DOMPurify to sanitize markdown HTML output - Remove unnecessary type assertion - Improve type safety across resource handling Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- packages/learningmap/package.json | 2 ++ packages/learningmap/src/Drawer.tsx | 10 +++++---- .../src/EditorDrawerTaskContent.tsx | 4 ++-- pnpm-lock.yaml | 21 +++++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/learningmap/package.json b/packages/learningmap/package.json index 389651a..b0314ef 100644 --- a/packages/learningmap/package.json +++ b/packages/learningmap/package.json @@ -34,7 +34,9 @@ }, "dependencies": { "@szhsin/react-menu": "^4.5.0", + "@types/dompurify": "^3.2.0", "@xyflow/react": "^12.8.6", + "dompurify": "^3.3.0", "elkjs": "^0.11.0", "fast-deep-equal": "^3.1.3", "html-to-image": "1.11.13", diff --git a/packages/learningmap/src/Drawer.tsx b/packages/learningmap/src/Drawer.tsx index 4276662..ab69093 100644 --- a/packages/learningmap/src/Drawer.tsx +++ b/packages/learningmap/src/Drawer.tsx @@ -1,11 +1,12 @@ import { Node } from "@xyflow/react"; -import { NodeData } from "./types"; +import { NodeData, Resource } from "./types"; import { X, Lock, CheckCircle } from "lucide-react"; import { Video } from "./Video"; import StarCircle from "./icons/StarCircle"; import { getTranslations } from "./translations"; import { marked } from "marked"; import { useMemo } from "react"; +import DOMPurify from "dompurify"; interface DrawerProps { open: boolean; @@ -59,10 +60,11 @@ function getCompletionOptional(node: Node, nodes: Node[]): N export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, language = "en" }: DrawerProps) { const t = getTranslations(language); - // Parse markdown description + // Parse markdown description and sanitize HTML const descriptionHtml = useMemo(() => { if (!node.data?.description) return ''; - return marked.parse(node.data.description, { async: false }) as string; + const rawHtml = marked.parse(node.data.description, { async: false }); + return DOMPurify.sanitize(rawHtml); }, [node.data?.description]); if (!open) return null; @@ -111,7 +113,7 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang
{t.resourcesLabel}
    - {node.data?.resources.map((r: any, idx: number) => { + {node.data?.resources.map((r: Resource, idx: number) => { if (r.type === "book") { return (
  • diff --git a/packages/learningmap/src/EditorDrawerTaskContent.tsx b/packages/learningmap/src/EditorDrawerTaskContent.tsx index 421c085..0442123 100644 --- a/packages/learningmap/src/EditorDrawerTaskContent.tsx +++ b/packages/learningmap/src/EditorDrawerTaskContent.tsx @@ -1,6 +1,6 @@ import { Node } from "@xyflow/react"; import { Plus, Trash2 } from "lucide-react"; -import { NodeData } from "./types"; +import { NodeData, Resource } from "./types"; import { getTranslations } from "./translations"; interface Props { @@ -131,7 +131,7 @@ export function EditorDrawerTaskContent({
- {(localNode.data.resources || []).map((resource: any, idx: number) => { + {(localNode.data.resources || []).map((resource: Resource, idx: number) => { const isBook = resource.type === "book"; return (
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c79d202..a9a4c12 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,9 +62,15 @@ importers: '@szhsin/react-menu': specifier: ^4.5.0 version: 4.5.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@types/dompurify': + specifier: ^3.2.0 + version: 3.2.0 '@xyflow/react': specifier: ^12.8.6 version: 12.9.0(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + dompurify: + specifier: ^3.3.0 + version: 3.3.0 elkjs: specifier: ^0.11.0 version: 0.11.0 @@ -1306,6 +1312,10 @@ packages: '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/dompurify@3.2.0': + resolution: {integrity: sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==} + deprecated: This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed. + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -1837,6 +1847,9 @@ packages: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} + dompurify@3.3.0: + resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dotenv@8.6.0: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} @@ -4761,6 +4774,10 @@ snapshots: '@types/deep-eql@4.0.2': {} + '@types/dompurify@3.2.0': + dependencies: + dompurify: 3.3.0 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -5363,6 +5380,10 @@ snapshots: dependencies: path-type: 4.0.0 + dompurify@3.3.0: + optionalDependencies: + '@types/trusted-types': 2.0.7 + dotenv@8.6.0: {} dunder-proto@1.0.1: From 5d453ce9728dd752e5e7287f49164aca93f7eaf5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:37:48 +0000 Subject: [PATCH 4/7] Fix null reference error in preview mode - Add null check for node in useMemo before accessing node.data - Update early return condition to check both open and node - Fixes "can't access property 'data', o is null" error Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- packages/learningmap/src/Drawer.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/learningmap/src/Drawer.tsx b/packages/learningmap/src/Drawer.tsx index ab69093..d8a7f37 100644 --- a/packages/learningmap/src/Drawer.tsx +++ b/packages/learningmap/src/Drawer.tsx @@ -62,12 +62,12 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang // Parse markdown description and sanitize HTML const descriptionHtml = useMemo(() => { - if (!node.data?.description) return ''; + if (!node || !node.data?.description) return ''; const rawHtml = marked.parse(node.data.description, { async: false }); return DOMPurify.sanitize(rawHtml); - }, [node.data?.description]); + }, [node, node?.data?.description]); - if (!open) return null; + if (!open || !node) return null; const locked = node.data?.state === 'locked' || false; const unlocked = node.data?.state === 'unlocked' || false; From fcd82732321261fa16525456d93d9ce7cdefcb93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 12:49:45 +0000 Subject: [PATCH 5/7] Add icons to resources and improve book format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add 🌐 icon before URL resources - Add 📚 icon before book resources - Format book resources as: 📚 Label (Name, Location) - Handle empty name field properly (no comma when name is empty) Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- packages/learningmap/src/Drawer.tsx | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/learningmap/src/Drawer.tsx b/packages/learningmap/src/Drawer.tsx index d8a7f37..ec0891f 100644 --- a/packages/learningmap/src/Drawer.tsx +++ b/packages/learningmap/src/Drawer.tsx @@ -115,17 +115,22 @@ export function Drawer({ open, onClose, onUpdate, node, nodes, onNodeClick, lang
    {node.data?.resources.map((r: Resource, idx: number) => { if (r.type === "book") { + // Format: 📚 Label (Name, Location) + // If name is empty, no comma should be visible + const bookDetails = []; + if (r.bookName) bookDetails.push(r.bookName); + if (r.bookLocation) bookDetails.push(r.bookLocation); + const detailsText = bookDetails.length > 0 ? ` (${bookDetails.join(', ')})` : ''; + return (
  • - {r.label} - {r.bookName &&
    📚 {r.bookName}
    } - {r.bookLocation &&
    📍 {r.bookLocation}
    } + 📚 {r.label}{detailsText}
  • ); } return (
  • - {r.url ? ( + 🌐 {r.url ? ( {r.label} ) : ( {r.label} From 748f6d0a99249db8271e886121e8f259dd89174c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 1 Nov 2025 13:01:04 +0000 Subject: [PATCH 6/7] Implement additional UX improvements 1. Use normal font for resource list (not Concert One) 2. Center node labels and add configurable font size 3. Add default edge type and color settings with bulk update Changes: - Resource list now uses system font for better readability - Node labels centered within nodes (flex center) - Added fontSize field to NodeData (configurable per node) - Added defaultEdgeType and defaultEdgeColor to Settings - New controls in Settings drawer for edge defaults - Button to update all edges to default settings - EditorCanvas uses default edge settings from settings Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- packages/learningmap/src/EditorCanvas.tsx | 4 +- .../src/EditorDrawerTaskContent.tsx | 11 ++++ packages/learningmap/src/SettingsDrawer.tsx | 51 +++++++++++++++++++ packages/learningmap/src/index.css | 4 ++ packages/learningmap/src/nodes/TaskNode.tsx | 14 ++--- packages/learningmap/src/nodes/TopicNode.tsx | 14 ++--- packages/learningmap/src/types.ts | 3 ++ 7 files changed, 85 insertions(+), 16 deletions(-) diff --git a/packages/learningmap/src/EditorCanvas.tsx b/packages/learningmap/src/EditorCanvas.tsx index 136af1f..c85807a 100644 --- a/packages/learningmap/src/EditorCanvas.tsx +++ b/packages/learningmap/src/EditorCanvas.tsx @@ -102,10 +102,10 @@ export const EditorCanvas = memo(({ defaultLanguage = "en" }: EditorCanvasProps) const defaultEdgeOptions = { animated: false, style: { - stroke: "#94a3b8", + stroke: settings?.defaultEdgeColor || "#94a3b8", strokeWidth: 2, }, - type: "default", + type: settings?.defaultEdgeType || "default", }; return ( diff --git a/packages/learningmap/src/EditorDrawerTaskContent.tsx b/packages/learningmap/src/EditorDrawerTaskContent.tsx index 0442123..0fab2ca 100644 --- a/packages/learningmap/src/EditorDrawerTaskContent.tsx +++ b/packages/learningmap/src/EditorDrawerTaskContent.tsx @@ -93,6 +93,17 @@ export function EditorDrawerTaskContent({ placeholder={t.placeholderNodeLabel} />
+
+ + handleFieldChange("fontSize", parseInt(e.target.value) || 14)} + placeholder="14" + min="8" + max="72" + /> +
= ({ // Get state from store const isOpen = useEditorStore(state => state.settingsDrawerOpen); const settings = useEditorStore(state => state.settings); + const edges = useEditorStore(state => state.edges); + const setEdges = useEditorStore(state => state.setEdges); // Get actions from store const setSettingsDrawerOpen = useEditorStore(state => state.setSettingsDrawerOpen); @@ -57,6 +59,22 @@ export const SettingsDrawer: React.FC = ({ })); }; + const handleUpdateAllEdges = () => { + const defaultType = localSettings?.defaultEdgeType || "default"; + const defaultColor = localSettings?.defaultEdgeColor || "#94a3b8"; + + const updatedEdges = edges.map(edge => ({ + ...edge, + type: defaultType, + style: { + ...edge.style, + stroke: defaultColor, + } + })); + + setEdges(updatedEdges); + }; + return ( <>
@@ -174,6 +192,39 @@ export const SettingsDrawer: React.FC = ({ {t.useCurrentViewport}
+ +
+ + +
+ +
+ setLocalSettings(settings => ({ ...settings, defaultEdgeColor: color }))} + /> +
+ +
+ +
diff --git a/packages/learningmap/src/index.css b/packages/learningmap/src/index.css index bc2207b..206b323 100644 --- a/packages/learningmap/src/index.css +++ b/packages/learningmap/src/index.css @@ -233,6 +233,10 @@ header.drawer-header { padding: 24px; } +.drawer-resources ul { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; +} + .drawer-description { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; line-height: 1.6; diff --git a/packages/learningmap/src/nodes/TaskNode.tsx b/packages/learningmap/src/nodes/TaskNode.tsx index 1960629..03f0577 100644 --- a/packages/learningmap/src/nodes/TaskNode.tsx +++ b/packages/learningmap/src/nodes/TaskNode.tsx @@ -7,16 +7,16 @@ export const TaskNode = ({ data, selected, isConnectable, ...props }: Node {isConnectable && } -
-
+
+
{data.label || "Untitled"}
+ {data.summary && ( +
+ {data.summary} +
+ )}
- {data.summary && ( -
- {data.summary} -
- )} {["Bottom", "Top", "Left", "Right"].map((pos) => ( ) => <> {isConnectable && } {data.state === "mastered" && } -
-
+
+
{data.label || "Untitled"}
+ {data.summary && ( +
+ {data.summary} +
+ )}
- {data.summary && ( -
- {data.summary} -
- )} {["Bottom", "Top", "Left", "Right"].map((pos) => ( Date: Sat, 1 Nov 2025 13:07:13 +0000 Subject: [PATCH 7/7] Fix description textarea to use system font - Changed textarea font-family from 'inherit' to system font stack - Ensures description input uses readable font consistently Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com> --- packages/learningmap/src/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/learningmap/src/index.css b/packages/learningmap/src/index.css index 206b323..21ce9b2 100644 --- a/packages/learningmap/src/index.css +++ b/packages/learningmap/src/index.css @@ -385,7 +385,7 @@ header.drawer-header { .form-group textarea { resize: vertical; - font-family: inherit; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } /* Buttons */