@ankorar/nodex 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +228 -0
  2. package/package.json +54 -0
  3. package/src/components/mindMap/Background.tsx +39 -0
  4. package/src/components/mindMap/Board.tsx +159 -0
  5. package/src/components/mindMap/CentalNode.tsx +121 -0
  6. package/src/components/mindMap/DefaultNode.tsx +205 -0
  7. package/src/components/mindMap/Header.tsx +247 -0
  8. package/src/components/mindMap/ImageNode.tsx +345 -0
  9. package/src/components/mindMap/KeyboardHelpDialog.tsx +108 -0
  10. package/src/components/mindMap/MineMap.tsx +237 -0
  11. package/src/components/mindMap/NodeStylePopover.tsx +486 -0
  12. package/src/components/mindMap/Nodes.tsx +113 -0
  13. package/src/components/mindMap/Nodex.tsx +65 -0
  14. package/src/components/mindMap/SaveStatusIndicator.tsx +61 -0
  15. package/src/components/mindMap/Segments.tsx +270 -0
  16. package/src/components/mindMap/ZenCard.tsx +41 -0
  17. package/src/components/ui/dialog.tsx +141 -0
  18. package/src/components/ui/popover.tsx +46 -0
  19. package/src/components/ui/select.tsx +192 -0
  20. package/src/components/ui/toggle-group.tsx +83 -0
  21. package/src/components/ui/toggle.tsx +45 -0
  22. package/src/config/rootKeyBinds.ts +191 -0
  23. package/src/config/shortCuts.ts +28 -0
  24. package/src/contexts/MindMapNodeEditorContext.tsx +47 -0
  25. package/src/handlers/rootKeyBinds/handleAltEKeyBind.ts +6 -0
  26. package/src/handlers/rootKeyBinds/handleAltHKeyBind.ts +6 -0
  27. package/src/handlers/rootKeyBinds/handleAltWKeyBind.ts +6 -0
  28. package/src/handlers/rootKeyBinds/handleAltZKeyBind.ts +6 -0
  29. package/src/handlers/rootKeyBinds/handleArrowHorizontalRootKeyBind.ts +46 -0
  30. package/src/handlers/rootKeyBinds/handleArrowVerticalRootKeyBind.ts +44 -0
  31. package/src/handlers/rootKeyBinds/handleBackEspaceKeyBind.ts +12 -0
  32. package/src/handlers/rootKeyBinds/handleERootKeyBind.ts +16 -0
  33. package/src/handlers/rootKeyBinds/handleEnterRootKeyBind.ts +35 -0
  34. package/src/handlers/rootKeyBinds/handleEscapeKeyBind.ts +24 -0
  35. package/src/handlers/rootKeyBinds/handleEspaceKeyBind.ts +11 -0
  36. package/src/handlers/rootKeyBinds/handleMoveByWorldKeyBind.ts +6 -0
  37. package/src/handlers/rootKeyBinds/handleRedoRootKeyBind.ts +23 -0
  38. package/src/handlers/rootKeyBinds/handleTabRootKeyBind.ts +49 -0
  39. package/src/handlers/rootKeyBinds/handleTransformNodeKeyBind.ts +39 -0
  40. package/src/handlers/rootKeyBinds/handleUndoRootKeyBind.ts +23 -0
  41. package/src/handlers/rootKeyBinds/handleZoonByKeyBind.ts +31 -0
  42. package/src/helpers/centerNode.ts +19 -0
  43. package/src/helpers/getNodeSide.ts +16 -0
  44. package/src/hooks/mindMap/useHelpers.tsx +9 -0
  45. package/src/hooks/mindMap/useMindMapDebounce.ts +47 -0
  46. package/src/hooks/mindMap/useMindMapHistoryDebounce.ts +69 -0
  47. package/src/hooks/mindMap/useMindMapNode.tsx +203 -0
  48. package/src/hooks/mindMap/useMindMapNodeEditor.ts +91 -0
  49. package/src/hooks/mindMap/useMindMapNodeMouseHandlers.ts +24 -0
  50. package/src/hooks/mindMap/useRootKeyBindHandlers.ts +49 -0
  51. package/src/hooks/mindMap/useRootMouseHandlers.ts +124 -0
  52. package/src/hooks/mindMap/useUpdateCenter.ts +54 -0
  53. package/src/index.ts +76 -0
  54. package/src/lib/utils.ts +6 -0
  55. package/src/state/mindMap.ts +793 -0
  56. package/src/state/mindMapHistory.ts +96 -0
  57. package/src/styles.input.css +95 -0
  58. package/src/utils/exportMindMapAsHighQualityImage.ts +327 -0
  59. package/src/utils/exportMindMapAsMarkdown.ts +102 -0
  60. package/src/utils/exportMindMapAsPdf.ts +241 -0
  61. package/src/utils/getMindMapPreviewDataUrl.ts +60 -0
  62. package/styles.css +2 -0
@@ -0,0 +1,91 @@
1
+ import type { FocusEvent, FormEvent, RefObject } from "react";
2
+ import { useLayoutEffect } from "react";
3
+ import { useMindMapState } from "../../state/mindMap";
4
+ import { useMindMapNode } from "./useMindMapNode";
5
+ import { useShallow } from "zustand/react/shallow";
6
+
7
+ type UseMindMapNodeEditorParams = {
8
+ nodeId: string;
9
+ text: string;
10
+ textRef: RefObject<HTMLElement | null>;
11
+ };
12
+
13
+ type UseMindMapNodeEditorResult = {
14
+ onFocus: () => void;
15
+ onBlur: (event: FocusEvent<HTMLElement>) => void;
16
+ onInput: (event: FormEvent<HTMLElement>) => void;
17
+ };
18
+
19
+ export function useMindMapNodeEditor({
20
+ nodeId,
21
+ text,
22
+ textRef,
23
+ }: UseMindMapNodeEditorParams): UseMindMapNodeEditorResult {
24
+ const { node } = useMindMapNode({ nodeId });
25
+ const { editingNodeId, setEditingNode, readOnly } = useMindMapState(
26
+ useShallow((state) => ({
27
+ editingNodeId: state.editingNodeId,
28
+ setEditingNode: state.setEditingNode,
29
+ readOnly: state.readOnly,
30
+ })),
31
+ );
32
+
33
+ const isEditing = editingNodeId === nodeId;
34
+
35
+ useLayoutEffect(() => {
36
+ const element = textRef.current;
37
+ if (!element) {
38
+ return;
39
+ }
40
+ if (!isEditing && element.textContent !== text) {
41
+ element.textContent = text;
42
+ }
43
+ }, [text, textRef, isEditing]);
44
+
45
+ useLayoutEffect(() => {
46
+ const element = textRef.current;
47
+ if (!element || !isEditing) {
48
+ return;
49
+ }
50
+ const focusText = () => {
51
+ element.focus();
52
+ const selection = window.getSelection();
53
+ if (!selection) {
54
+ return;
55
+ }
56
+ const range = document.createRange();
57
+ range.selectNodeContents(element);
58
+ range.collapse(false);
59
+ selection.removeAllRanges();
60
+ selection.addRange(range);
61
+ };
62
+ requestAnimationFrame(focusText);
63
+ }, [isEditing, textRef]);
64
+
65
+ return {
66
+ onFocus: () => {
67
+ node?.select();
68
+ if (!readOnly) {
69
+ node?.edit();
70
+ }
71
+ },
72
+ onBlur: () => {
73
+ setEditingNode(null);
74
+ },
75
+ onInput: (event) => {
76
+ if (readOnly) {
77
+ return;
78
+ }
79
+
80
+ if (!node) {
81
+ return;
82
+ }
83
+ const element = (event.currentTarget ??
84
+ textRef.current) as HTMLElement | null;
85
+ node
86
+ .chain()
87
+ .updateText(element?.textContent ?? "")
88
+ .commit();
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,24 @@
1
+ import { type MouseEvent } from "react";
2
+ import { useMindMapNode } from "./useMindMapNode";
3
+ import { useMindMapState } from "../../state/mindMap";
4
+
5
+ export function useMindMapNodeMouseHandlers(nodeId: string) {
6
+ const { node } = useMindMapNode({ nodeId });
7
+ const readOnly = useMindMapState((state) => state.readOnly);
8
+
9
+ return {
10
+ onMouseDown: (event: MouseEvent<HTMLDivElement>) => {
11
+ event.stopPropagation();
12
+ if (!readOnly) {
13
+ node?.select();
14
+ }
15
+ },
16
+ onDoubleClick: (event: MouseEvent<HTMLDivElement>) => {
17
+ event.stopPropagation();
18
+ if (!readOnly) {
19
+ node?.select();
20
+ node?.edit();
21
+ }
22
+ },
23
+ };
24
+ }
@@ -0,0 +1,49 @@
1
+ import { useEffect } from "react";
2
+ import { useShallow } from "zustand/react/shallow";
3
+ import { useMindMapState } from "../../state/mindMap";
4
+ import { type RootKeyBinds, rootKeyBinds } from "../../config/rootKeyBinds";
5
+
6
+ export function useRootKeyBindHandlers() {
7
+ const { editingNodeId, readOnly } = useMindMapState(
8
+ useShallow((state) => ({
9
+ editingNodeId: state.editingNodeId,
10
+ readOnly: state.readOnly,
11
+ }))
12
+ );
13
+
14
+ useEffect(() => {
15
+ const handleKeyDown = (e: KeyboardEvent) => {
16
+ if (readOnly) {
17
+ return;
18
+ }
19
+
20
+ const isEditing = !!editingNodeId;
21
+ let key = e.key;
22
+
23
+ if (e.ctrlKey) {
24
+ key = "Ctrl+" + e.key;
25
+ }
26
+
27
+ if (e.altKey) {
28
+ key = "Alt+" + e.key;
29
+ }
30
+
31
+ const keyBind = rootKeyBinds[key as RootKeyBinds];
32
+
33
+ if (keyBind?.skipOnEditing && isEditing) {
34
+ return;
35
+ }
36
+
37
+ if (keyBind?.handler) {
38
+ e.preventDefault();
39
+ keyBind.handler();
40
+ }
41
+ };
42
+
43
+ const listenerOptions = { capture: true };
44
+ document.addEventListener("keydown", handleKeyDown, listenerOptions);
45
+ return () => {
46
+ document.removeEventListener("keydown", handleKeyDown, listenerOptions);
47
+ };
48
+ }, [editingNodeId, readOnly]);
49
+ }
@@ -0,0 +1,124 @@
1
+ import type {
2
+ MouseEvent,
3
+ RefObject,
4
+ WheelEvent as WheelReactEvent,
5
+ } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
+
8
+ import { useMindMapState } from "../../state/mindMap";
9
+ import { useShallow } from "zustand/react/shallow";
10
+
11
+ interface UseRootMouseHandlersProps {
12
+ rootRef: RefObject<HTMLDivElement | null>;
13
+ }
14
+
15
+ export function useRootMouseHandlers({ rootRef }: UseRootMouseHandlersProps) {
16
+ const {
17
+ offset,
18
+ scale,
19
+ clampScale,
20
+ setScale,
21
+ setOffset,
22
+ setSelectedNode,
23
+ setEditingNode,
24
+ selectedNodeId,
25
+ helpOpen,
26
+ } = useMindMapState(
27
+ useShallow((state) => ({
28
+ offset: state.offset,
29
+ scale: state.scale,
30
+ clampScale: state.clampScale,
31
+ setScale: state.setScale,
32
+ setOffset: state.setOffset,
33
+ setSelectedNode: state.setSelectedNode,
34
+ setEditingNode: state.setEditingNode,
35
+ selectedNodeId: state.selectedNodeId,
36
+ helpOpen: state.helpOpen,
37
+ })),
38
+ );
39
+ const isDraggingRef = useRef(false);
40
+ const [isDragging, setIsDragging] = useState(false);
41
+ const dragStartRef = useRef({ x: 0, y: 0 });
42
+ const offsetStartRef = useRef({ x: 0, y: 0 });
43
+
44
+ const onWheel = (event: WheelReactEvent<HTMLDivElement>) => {
45
+ event.preventDefault();
46
+ if (helpOpen) return;
47
+ const zoomSpeed = event.ctrlKey ? 0.0005 : 0.0005;
48
+ const zoomDelta = -event.deltaY * zoomSpeed;
49
+ const nextScale = clampScale(scale + zoomDelta);
50
+ if (nextScale === scale) {
51
+ return;
52
+ }
53
+ const bounds = event.currentTarget.getBoundingClientRect();
54
+ const pointerX = event.clientX - bounds.left;
55
+ const pointerY = event.clientY - bounds.top;
56
+ const worldX = (pointerX - offset.x) / scale;
57
+ const worldY = (pointerY - offset.y) / scale;
58
+ const nextOffset = {
59
+ x: pointerX - worldX * nextScale,
60
+ y: pointerY - worldY * nextScale,
61
+ };
62
+ setScale(nextScale);
63
+ setOffset(nextOffset);
64
+ };
65
+
66
+ useEffect(() => {
67
+ const element = rootRef.current;
68
+ if (!element) return;
69
+
70
+ const handleWheel = (event: WheelEvent) => {
71
+ const e = event as unknown as WheelReactEvent<HTMLDivElement>;
72
+ onWheel(e);
73
+ };
74
+
75
+ element.addEventListener("wheel", handleWheel, { passive: false });
76
+
77
+ return () => {
78
+ element.removeEventListener("wheel", handleWheel);
79
+ };
80
+ }, [onWheel]);
81
+
82
+ return {
83
+ isDragging,
84
+ onMouseDown: (event: MouseEvent<HTMLDivElement>) => {
85
+ const target = event.target as Element | null;
86
+ const clickedNode = target?.closest?.("[data-nodex-node]");
87
+ const clickedUi = target?.closest?.("[data-nodex-ui]");
88
+ if (selectedNodeId && !clickedNode && !clickedUi) {
89
+ setSelectedNode(null);
90
+ setEditingNode(null);
91
+ }
92
+ if (clickedNode || clickedUi) {
93
+ return;
94
+ }
95
+ if (event.button !== 0) {
96
+ return;
97
+ }
98
+ isDraggingRef.current = true;
99
+ setIsDragging(true);
100
+ dragStartRef.current = { x: event.clientX, y: event.clientY };
101
+ offsetStartRef.current = { x: offset.x, y: offset.y };
102
+ },
103
+ onMouseMove: (event: MouseEvent<HTMLDivElement>) => {
104
+ if (!isDraggingRef.current) {
105
+ return;
106
+ }
107
+ const deltaX = event.clientX - dragStartRef.current.x;
108
+ const deltaY = event.clientY - dragStartRef.current.y;
109
+ setOffset({
110
+ x: offsetStartRef.current.x + deltaX,
111
+ y: offsetStartRef.current.y + deltaY,
112
+ });
113
+ },
114
+ onMouseUp: () => {
115
+ isDraggingRef.current = false;
116
+ setIsDragging(false);
117
+ },
118
+ onMouseLeave: () => {
119
+ isDraggingRef.current = false;
120
+ setIsDragging(false);
121
+ },
122
+ onWheel,
123
+ };
124
+ }
@@ -0,0 +1,54 @@
1
+ import { useMindMapState } from "../../state/mindMap";
2
+ import { type RefObject, useEffect } from "react";
3
+ import { useShallow } from "zustand/react/shallow";
4
+ import { useMindMapNode } from "./useMindMapNode";
5
+ import { useHelpers } from "./useHelpers";
6
+
7
+ interface UseUpdateCenterProps {
8
+ rootRef: RefObject<HTMLDivElement | null>;
9
+ }
10
+
11
+ export function useUpdateCenter({ rootRef }: UseUpdateCenterProps) {
12
+ const helpers = useHelpers();
13
+ const { getCentralNode, setOffset, editingNodeId } = useMindMapState(
14
+ useShallow((state) => ({
15
+ getCentralNode: state.getCentralNode,
16
+ setOffset: state.setOffset,
17
+ editingNodeId: state.editingNodeId,
18
+ }))
19
+ );
20
+
21
+ const { node } = useMindMapNode({ nodeId: editingNodeId });
22
+
23
+ useEffect(() => {
24
+ if (node && node.text === "") {
25
+ helpers.centerNode(node);
26
+ }
27
+ }, [node]);
28
+
29
+ useEffect(() => {
30
+ const updateCenter = () => {
31
+ const element = rootRef.current;
32
+ if (!element) return;
33
+
34
+ const centralNode = getCentralNode();
35
+ if (!centralNode) return;
36
+
37
+ const centralPos = centralNode.pos;
38
+ const centralSize = centralNode.style;
39
+ const bounds = element.getBoundingClientRect();
40
+
41
+ setOffset({
42
+ x: bounds.width / 2 - (centralPos.x + centralSize.w / 2),
43
+ y: bounds.height / 2 - (centralPos.y + centralSize.h / 2),
44
+ });
45
+ };
46
+
47
+ updateCenter();
48
+ window.addEventListener("resize", updateCenter);
49
+
50
+ return () => {
51
+ window.removeEventListener("resize", updateCenter);
52
+ };
53
+ }, [setOffset]);
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,76 @@
1
+ export {
2
+ useMindMapState,
3
+ layoutMindMapNodes,
4
+ wrapTextAtWords,
5
+ applyBranchColorsToNodes,
6
+ } from "./state/mindMap";
7
+ export {
8
+ useMindMapHistory,
9
+ createMindMapSnapshot,
10
+ clearMindMapHistory,
11
+ } from "./state/mindMapHistory";
12
+ export { useMindMapDebounce } from "./hooks/mindMap/useMindMapDebounce";
13
+ export { getMindMapPreviewDataUrl } from "./utils/getMindMapPreviewDataUrl";
14
+ export {
15
+ createMindMapExportCanvas,
16
+ exportMindMapAsHighQualityImage,
17
+ HIGH_QUALITY_EXPORT_SCALE,
18
+ type CreateMindMapCanvasOptions,
19
+ type ExportImageOptions,
20
+ } from "./utils/exportMindMapAsHighQualityImage";
21
+ export {
22
+ exportMindMapAsMarkdown,
23
+ mindMapToMarkdown,
24
+ type ExportMarkdownOptions,
25
+ } from "./utils/exportMindMapAsMarkdown";
26
+ export {
27
+ exportMindMapAsPdf,
28
+ type ExportPdfOptions,
29
+ } from "./utils/exportMindMapAsPdf";
30
+ export { useMindMapHistoryDebounce } from "./hooks/mindMap/useMindMapHistoryDebounce";
31
+
32
+ export { Background, type BackgroundProps } from "./components/mindMap/Background";
33
+ export {
34
+ Board,
35
+ type BoardProps,
36
+ type BoardStyleSlots,
37
+ } from "./components/mindMap/Board";
38
+ export { CentalNode } from "./components/mindMap/CentalNode";
39
+ export { DefaultNode } from "./components/mindMap/DefaultNode";
40
+ export {
41
+ Header as MindMapHeader,
42
+ type HeaderProps,
43
+ type HeaderStyleSlots,
44
+ } from "./components/mindMap/Header";
45
+ export {
46
+ SaveStatusIndicator,
47
+ type MindMapSaveStatus,
48
+ } from "./components/mindMap/SaveStatusIndicator";
49
+ export { ImageNode } from "./components/mindMap/ImageNode";
50
+ export {
51
+ KeyboardHelpDialog,
52
+ type KeyboardHelpDialogProps,
53
+ type KeyboardHelpDialogStyleSlots,
54
+ } from "./components/mindMap/KeyboardHelpDialog";
55
+ export {
56
+ MineMap,
57
+ type MineMapStyleSlots,
58
+ } from "./components/mindMap/MineMap";
59
+ export {
60
+ NodeStylePopover,
61
+ type NodeStylePopoverColorOption,
62
+ type NodeStylePopoverStyleSlots,
63
+ } from "./components/mindMap/NodeStylePopover";
64
+ export { Nodes } from "./components/mindMap/Nodes";
65
+ export { Nodex } from "./components/mindMap/Nodex";
66
+ export { Segments } from "./components/mindMap/Segments";
67
+ export { ZenCard, type ZenCardProps } from "./components/mindMap/ZenCard";
68
+
69
+ export type {
70
+ MindMapNode,
71
+ MindMapNodeStyle,
72
+ MindMapNodeType,
73
+ MindMapNodeTextAlign,
74
+ MindMapNodeFontSize,
75
+ } from "./state/mindMap";
76
+ export type { NodeEditorCustomButton } from "./contexts/MindMapNodeEditorContext";
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }