Skip to content

Commit 4efdb72

Browse files
Copilotmikebarkmin
andcommitted
Implement font size selection, zIndex, and image captions
- Add font size constants (S=10px, M=14px, L=18px, XL=24px) - Replace number input with dropdown for font size selection - Apply proper zIndex values: Image=10, Text=20, Topic/Task=30 - Add caption support to images with markdown link parsing - Update translations for all new features (en/de) Co-authored-by: mikebarkmin <2592379+mikebarkmin@users.noreply.github.com>
1 parent 41cb599 commit 4efdb72

11 files changed

Lines changed: 151 additions & 13 deletions

packages/learningmap/src/EditorDrawerImageContent.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ export function EditorDrawerImageContent({ localNode, handleFieldChange, languag
3434
onChange={handleFileChange}
3535
/>
3636
</div>
37+
<div className="form-group">
38+
<label>{t.caption}</label>
39+
<input
40+
type="text"
41+
value={localNode.data.caption || ""}
42+
onChange={(e) => handleFieldChange("caption", e.target.value)}
43+
placeholder={t.placeholderImageCaption}
44+
/>
45+
</div>
3746
{localNode.data.data && (
3847
<div style={{ marginTop: 16 }}>
3948
<label>Preview:</label>

packages/learningmap/src/EditorDrawerTaskContent.tsx

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Node } from "@xyflow/react";
22
import { Plus, Trash2 } from "lucide-react";
33
import { NodeData, Resource } from "./types";
44
import { getTranslations } from "./translations";
5+
import { FONT_SIZE_VALUES, getFontSizeOption, FontSizeOption } from "./fontSizes";
56

67
interface Props {
78
localNode: Node<NodeData>;
@@ -94,15 +95,19 @@ export function EditorDrawerTaskContent({
9495
/>
9596
</div>
9697
<div className="form-group">
97-
<label>Font Size (px)</label>
98-
<input
99-
type="number"
100-
value={localNode.data.fontSize || 14}
101-
onChange={(e) => handleFieldChange("fontSize", parseInt(e.target.value) || 14)}
102-
placeholder="14"
103-
min="8"
104-
max="72"
105-
/>
98+
<label>{t.fontSize}</label>
99+
<select
100+
value={getFontSizeOption(localNode.data.fontSize)}
101+
onChange={(e) => {
102+
const selectedSize = e.target.value as FontSizeOption;
103+
handleFieldChange("fontSize", FONT_SIZE_VALUES[selectedSize]);
104+
}}
105+
>
106+
<option value="S">{t.fontSizeSmall} ({FONT_SIZE_VALUES.S}px)</option>
107+
<option value="M">{t.fontSizeMedium} ({FONT_SIZE_VALUES.M}px)</option>
108+
<option value="L">{t.fontSizeLarge} ({FONT_SIZE_VALUES.L}px)</option>
109+
<option value="XL">{t.fontSizeXLarge} ({FONT_SIZE_VALUES.XL}px)</option>
110+
</select>
106111
</div>
107112
<div className="form-group">
108113
<label>{t.summary}</label>

packages/learningmap/src/EditorToolbar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Node, useReactFlow } from "@xyflow/react";
99
import { NodeData } from "./types";
1010
import { useJsonStore } from "./useJsonStore";
1111
import { useFileOperations } from "./useFileOperations";
12+
import { getZIndexForNodeType } from "./zIndexHelper";
1213

1314
interface EditorToolbarProps {
1415
defaultLanguage?: string;
@@ -56,6 +57,7 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
5657
id: `node-${Date.now()}`,
5758
type,
5859
position,
60+
zIndex: getZIndexForNodeType(type),
5961
data: {
6062
label: type === "task" ? t.newTask : type === "topic" ? t.newTopic : type,
6163
state: "unlocked",

packages/learningmap/src/editorStore.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Connection,
1717
} from "@xyflow/react";
1818
import { NodeData, RoadmapData, Settings } from "./types";
19+
import { getZIndexForNodeType } from "./zIndexHelper";
1920

2021
// Note: This is a global store for the editor. Typically only one editor instance is active at a time.
2122
// If you need multiple independent editor instances, consider creating store instances per component or using context.
@@ -370,6 +371,8 @@ export const useEditorStore = create<EditorState>()(
370371
...n,
371372
draggable: true,
372373
className: n.data.color ? n.data.color : n.className,
374+
// Ensure zIndex is set based on node type if not already present
375+
zIndex: n.zIndex !== undefined ? n.zIndex : getZIndexForNodeType(n.type),
373376
data: { ...n.data },
374377
}));
375378

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Font size constants for node labels
2+
3+
export type FontSizeOption = "S" | "M" | "L" | "XL";
4+
5+
export const FONT_SIZE_VALUES: Record<FontSizeOption, number> = {
6+
S: 10,
7+
M: 14,
8+
L: 18,
9+
XL: 24,
10+
};
11+
12+
export const DEFAULT_FONT_SIZE: FontSizeOption = "M";
13+
14+
export function getFontSizeValue(size?: number | FontSizeOption): number {
15+
if (typeof size === "number") {
16+
// Convert old numeric values to closest size
17+
if (size <= 10) return FONT_SIZE_VALUES.S;
18+
if (size <= 14) return FONT_SIZE_VALUES.M;
19+
if (size <= 18) return FONT_SIZE_VALUES.L;
20+
return FONT_SIZE_VALUES.XL;
21+
}
22+
if (size && size in FONT_SIZE_VALUES) {
23+
return FONT_SIZE_VALUES[size as FontSizeOption];
24+
}
25+
return FONT_SIZE_VALUES[DEFAULT_FONT_SIZE];
26+
}
27+
28+
export function getFontSizeOption(value?: number): FontSizeOption {
29+
if (!value) return DEFAULT_FONT_SIZE;
30+
// Find closest match
31+
if (value <= 10) return "S";
32+
if (value <= 14) return "M";
33+
if (value <= 18) return "L";
34+
return "XL";
35+
}

packages/learningmap/src/nodes/ImageNode.tsx

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,55 @@
11
import { Node, NodeResizer } from "@xyflow/react";
22
import { ImageNodeData } from "../types";
33

4+
// Simple markdown link parser for captions
5+
function parseMarkdownLinks(text: string): React.ReactNode[] {
6+
const parts: React.ReactNode[] = [];
7+
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
8+
let lastIndex = 0;
9+
let match;
10+
11+
while ((match = linkRegex.exec(text)) !== null) {
12+
// Add text before the link
13+
if (match.index > lastIndex) {
14+
parts.push(text.substring(lastIndex, match.index));
15+
}
16+
// Add the link
17+
parts.push(
18+
<a key={match.index} href={match[2]} target="_blank" rel="noopener noreferrer" style={{ color: "#3b82f6", textDecoration: "underline" }}>
19+
{match[1]}
20+
</a>
21+
);
22+
lastIndex = match.index + match[0].length;
23+
}
24+
25+
// Add remaining text
26+
if (lastIndex < text.length) {
27+
parts.push(text.substring(lastIndex));
28+
}
29+
30+
return parts.length > 0 ? parts : [text];
31+
}
32+
433
export const ImageNode = ({ data, selected }: Node<ImageNodeData>) => {
534
return (
635
<>
736
{data.data ? (
837
<>
938
<NodeResizer isVisible={selected} keepAspectRatio />
10-
<div style={{ display: "flex", justifyContent: "center", alignItems: "center", width: "100%", height: "100%", overflow: "hidden" }}>
39+
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", width: "100%", height: "100%", overflow: "hidden", gap: "4px" }}>
1140
<img
1241
src={data.data}
1342
style={{
1443
maxWidth: "100%",
15-
maxHeight: "100%",
44+
maxHeight: data.caption ? "calc(100% - 24px)" : "100%",
45+
objectFit: "contain",
1646
}}
1747
/>
48+
{data.caption && (
49+
<div style={{ fontSize: "11px", color: "#6b7280", textAlign: "center", padding: "0 4px", lineHeight: "1.3" }}>
50+
{parseMarkdownLinks(data.caption)}
51+
</div>
52+
)}
1853
</div>
1954
</>
2055
) : (

packages/learningmap/src/nodes/TaskNode.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Handle, Node, NodeResizer, Position } from "@xyflow/react";
22
import { NodeData } from "../types";
33
import { CircleCheck } from "lucide-react";
4+
import { getFontSizeValue } from "../fontSizes";
45

56
export const TaskNode = ({ data, selected, isConnectable, ...props }: Node<NodeData>) => {
67
return (
78
<>
89
{isConnectable && <NodeResizer isVisible={selected} />}
910
<CircleCheck className="icon" />
1011
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", textAlign: "center" }}>
11-
<div style={{ fontWeight: 600, fontSize: data.fontSize ? `${data.fontSize}px` : "14px" }}>
12+
<div style={{ fontWeight: 600, fontSize: `${getFontSizeValue(data.fontSize)}px` }}>
1213
{data.label || "Untitled"}
1314
</div>
1415
{data.summary && (

packages/learningmap/src/nodes/TopicNode.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { Handle, Node, NodeResizer, Position } from "@xyflow/react";
22
import { NodeData } from "../types";
33
import StarCircle from "../icons/StarCircle";
4+
import { getFontSizeValue } from "../fontSizes";
45

56
export const TopicNode = ({ data, selected, isConnectable }: Node<NodeData>) => {
67
return (
78
<>
89
{isConnectable && <NodeResizer isVisible={selected} />}
910
{data.state === "mastered" && <StarCircle className="icon" />}
1011
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", width: "100%", height: "100%", textAlign: "center" }}>
11-
<div style={{ fontWeight: 600, fontSize: data.fontSize ? `${data.fontSize}px` : "14px" }}>
12+
<div style={{ fontWeight: 600, fontSize: `${getFontSizeValue(data.fontSize)}px` }}>
1213
{data.label || "Untitled"}
1314
</div>
1415
{data.summary && (

packages/learningmap/src/translations.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,16 @@ export interface Translations {
203203
welcomeOpenFile: string;
204204
welcomeAddTopic: string;
205205
welcomeHelp: string;
206+
207+
// Font size options
208+
fontSizeSmall: string;
209+
fontSizeMedium: string;
210+
fontSizeLarge: string;
211+
fontSizeXLarge: string;
212+
213+
// Image caption
214+
caption: string;
215+
placeholderImageCaption: string;
206216
}
207217

208218
const en: Translations = {
@@ -414,6 +424,16 @@ const en: Translations = {
414424
welcomeOpenFile: "Open File",
415425
welcomeAddTopic: "Add Topic",
416426
welcomeHelp: "Help",
427+
428+
// Font size options
429+
fontSizeSmall: "Small",
430+
fontSizeMedium: "Medium",
431+
fontSizeLarge: "Large",
432+
fontSizeXLarge: "Extra Large",
433+
434+
// Image caption
435+
caption: "Caption",
436+
placeholderImageCaption: "Add caption (supports [markdown links](url))",
417437
};
418438

419439
const de: Translations = {
@@ -628,6 +648,16 @@ const de: Translations = {
628648
welcomeOpenFile: "Datei öffnen",
629649
welcomeAddTopic: "Thema hinzufügen",
630650
welcomeHelp: "Hilfe",
651+
652+
// Font size options
653+
fontSizeSmall: "Klein",
654+
fontSizeMedium: "Mittel",
655+
fontSizeLarge: "Groß",
656+
fontSizeXLarge: "Extra Groß",
657+
658+
// Image caption
659+
caption: "Bildunterschrift",
660+
placeholderImageCaption: "Bildunterschrift hinzufügen (unterstützt [Markdown-Links](url))",
631661
};
632662

633663
export const translations: Record<string, Translations> = {

packages/learningmap/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface NodeData {
3939

4040
export interface ImageNodeData {
4141
data?: string; // base64 encoded image
42+
caption?: string; // Caption with markdown support for links
4243
}
4344

4445
export interface TextNodeData {

0 commit comments

Comments
 (0)