@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
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @ankorar/nodex
2
+
3
+ Composable React package for building and extending a keyboard-driven mind map experience.
4
+
5
+ This package exposes **state, hooks, and UI primitives** so each consumer app can build its own screen composition without being locked to a single page component.
6
+
7
+ ## What This Package Provides
8
+
9
+ - Stateful mind map engine (Zustand-based)
10
+ - Ready-to-compose visual components (board, nodes, minimap, popovers)
11
+ - Keyboard and interaction handlers
12
+ - Debounce hooks for history/state flow
13
+ - Type exports for consumer-side extensions
14
+
15
+ ## Installation
16
+
17
+ ### Workspace (monorepo)
18
+
19
+ ```bash
20
+ pnpm add @ankorar/nodex --workspace
21
+ ```
22
+
23
+ ### Future npm install
24
+
25
+ ```bash
26
+ pnpm add @ankorar/nodex
27
+ ```
28
+
29
+ ## Quick Usage (Composition)
30
+
31
+ ```tsx
32
+ import "@ankorar/nodex/styles.css";
33
+ import {
34
+ Background,
35
+ Board,
36
+ MindMapHeader,
37
+ MineMap,
38
+ Nodex,
39
+ ZenCard,
40
+ useMindMapDebounce,
41
+ } from "@ankorar/nodex";
42
+
43
+ export function MindMapPage() {
44
+ useMindMapDebounce(() => undefined, { delayMs: 3000 });
45
+
46
+ return (
47
+ <section className="h-[calc(100dvh-11rem)] min-h-[38rem]">
48
+ <Nodex readOnly={false}>
49
+ <MindMapHeader title="Mind Map" />
50
+ <Board>
51
+ <Background />
52
+ <MineMap />
53
+ <ZenCard />
54
+ </Board>
55
+ </Nodex>
56
+ </section>
57
+ );
58
+ }
59
+ ```
60
+
61
+ ## Read-only Mode
62
+
63
+ Use `Nodex` with `readOnly` when the map should be visual-only:
64
+
65
+ ```tsx
66
+ <Nodex readOnly>
67
+ <MindMapHeader title="Mind Map (View)" />
68
+ <Board>
69
+ <Background />
70
+ <MineMap />
71
+ <ZenCard />
72
+ </Board>
73
+ </Nodex>
74
+ ```
75
+
76
+ In `readOnly` mode, the package blocks content mutations (keyboard shortcuts, inline edits, add/remove node actions, and style popover updates).
77
+
78
+ ## High-quality image export
79
+
80
+ You can export the whole map as a high-resolution PNG so that text stays readable when zooming, even on very large maps.
81
+
82
+ - **From the header**: set `showExportImageButton` on `MindMapHeader` to show an “Export image” button that downloads a PNG (scale factor 3× by default).
83
+ - **Programmatic**: use `exportMindMapAsHighQualityImage(nodes, options?)` with optional `scale` (1–4) and `filename`. Exported image dimensions are derived from node bounds plus padding, then multiplied by `scale`.
84
+
85
+ ```tsx
86
+ import { MindMapHeader, exportMindMapAsHighQualityImage, useMindMapState } from "@ankorar/nodex";
87
+
88
+ // In your page:
89
+ <MindMapHeader title="My Map" showExportImageButton />
90
+
91
+ // Or call manually:
92
+ const nodes = useMindMapState.getState().getFlatNodes();
93
+ await exportMindMapAsHighQualityImage(nodes, { scale: 3, filename: "my-map" });
94
+ ```
95
+
96
+ ## Initial State
97
+
98
+ `@ankorar/nodex` now starts with an empty node list (`nodes: []`).
99
+
100
+ If your app loads maps from an API, hydrate the state explicitly:
101
+
102
+ ```tsx
103
+ import { useEffect } from "react";
104
+ import { useMindMapState, type MindMapNode } from "@ankorar/nodex";
105
+
106
+ export function MindMapHydrator({ nodes }: { nodes: MindMapNode[] }) {
107
+ useEffect(() => {
108
+ useMindMapState.setState({
109
+ nodes,
110
+ selectedNodeId: null,
111
+ editingNodeId: null,
112
+ });
113
+ }, [nodes]);
114
+
115
+ return null;
116
+ }
117
+ ```
118
+
119
+ ## Styling
120
+
121
+ `@ankorar/nodex` ships its own precompiled stylesheet, so consumer apps do not need to compile Tailwind classes from this package.
122
+
123
+ Import it once at app entry:
124
+
125
+ ```tsx
126
+ import "@ankorar/nodex/styles.css";
127
+ ```
128
+
129
+ ## Public API
130
+
131
+ ### Components
132
+
133
+ - `Nodex`
134
+ - `Board`
135
+ - `Background`
136
+ - `Nodes`
137
+ - `Segments`
138
+ - `CentalNode`
139
+ - `DefaultNode`
140
+ - `ImageNode`
141
+ - `NodeStylePopover`
142
+ - `KeyboardHelpDialog`
143
+ - `MineMap`
144
+ - `ZenCard`
145
+ - `MindMapHeader` (optional: `showExportImageButton` for PNG export)
146
+
147
+ ### Utilities
148
+
149
+ - `getMindMapPreviewDataUrl(nodes)` – data URL for minimap-style preview thumbnails
150
+ - `exportMindMapAsHighQualityImage(nodes, options?)` – render map to high-resolution PNG and trigger download
151
+ - `HIGH_QUALITY_EXPORT_SCALE` – default scale factor (3) for export
152
+
153
+ ### Hooks
154
+
155
+ - `useMindMapDebounce`
156
+ - `useMindMapHistoryDebounce`
157
+
158
+ ### State
159
+
160
+ - `useMindMapState`
161
+ - `useMindMapHistory`
162
+ - `createMindMapSnapshot`
163
+
164
+ ### Types
165
+
166
+ - `MindMapNode`
167
+ - `MindMapNodeStyle`
168
+ - `MindMapNodeType`
169
+ - `MindMapNodeTextAlign`
170
+ - `MindMapNodeFontSize`
171
+
172
+ ## Package Structure
173
+
174
+ ```text
175
+ src/
176
+ components/
177
+ mindMap/
178
+ ui/
179
+ config/
180
+ handlers/
181
+ helpers/
182
+ hooks/
183
+ mindMap/
184
+ lib/
185
+ state/
186
+ index.ts
187
+ ```
188
+
189
+ ## Development
190
+
191
+ From the monorepo root:
192
+
193
+ ```bash
194
+ pnpm --filter @ankorar/nodex dev
195
+ ```
196
+
197
+ Available scripts in this package:
198
+
199
+ - `dev`: watches types and regenerates `styles.css`
200
+ - `build`: type validation + stylesheet build
201
+ - `lint`: Type validation (`tsc --noEmit`)
202
+
203
+ ## Consumer Responsibilities
204
+
205
+ Because this package is composition-first, the consumer app is responsible for:
206
+
207
+ - Defining the route/page shell
208
+ - Providing app-level layout and authentication wrappers
209
+ - Loading global styles/tokens expected by the selected UI setup
210
+
211
+ ## Versioning and Publishing
212
+
213
+ Before publishing a new version:
214
+
215
+ 1. Validate exports in `src/index.ts`.
216
+ 2. Run package checks:
217
+ - `pnpm --filter @ankorar/nodex build`
218
+ - `pnpm --filter @ankorar/nodex lint`
219
+ 3. Update docs for any API/behavior change.
220
+ 4. Bump package version in `packages/nodex/package.json`.
221
+
222
+ ## Documentation Policy
223
+
224
+ All package documentation must be written in **English**.
225
+
226
+ For mandatory documentation rules (including AI/chat workflows), see:
227
+
228
+ - `docs/AI_DOCUMENTATION_POLICY.md`
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@ankorar/nodex",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./src/index.ts",
10
+ "import": "./src/index.ts"
11
+ },
12
+ "./styles.css": "./styles.css"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "styles.css"
17
+ ],
18
+ "peerDependencies": {
19
+ "react": "19.2.0",
20
+ "react-dom": "19.2.0"
21
+ },
22
+ "dependencies": {
23
+ "@radix-ui/react-dialog": "1.1.15",
24
+ "@radix-ui/react-popover": "1.1.15",
25
+ "@radix-ui/react-select": "2.2.6",
26
+ "@radix-ui/react-toggle": "1.1.10",
27
+ "@radix-ui/react-toggle-group": "1.1.11",
28
+ "class-variance-authority": "0.7.1",
29
+ "clsx": "2.1.1",
30
+ "contrast": "1.0.1",
31
+ "jspdf": "2.5.2",
32
+ "lucide-react": "0.562.0",
33
+ "tailwind-merge": "3.4.0",
34
+ "zustand": "5.0.9"
35
+ },
36
+ "devDependencies": {
37
+ "@tailwindcss/cli": "4.1.0",
38
+ "@types/react": "19.2.5",
39
+ "@types/react-dom": "19.2.3",
40
+ "concurrently": "8.2.2",
41
+ "tailwindcss": "4.1.0",
42
+ "tw-animate-css": "1.4.0",
43
+ "typescript": "5.9.3"
44
+ },
45
+ "scripts": {
46
+ "dev": "concurrently \"pnpm run dev:types\" \"pnpm run dev:styles\"",
47
+ "dev:types": "tsc --watch --preserveWatchOutput --noEmit",
48
+ "dev:styles": "tailwindcss -i ./src/styles.input.css -o ./styles.css --watch",
49
+ "build": "pnpm run build:types && pnpm run build:styles",
50
+ "build:types": "tsc --noEmit",
51
+ "build:styles": "tailwindcss -i ./src/styles.input.css -o ./styles.css --minify",
52
+ "lint": "tsc --noEmit"
53
+ }
54
+ }
@@ -0,0 +1,39 @@
1
+ import type { CSSProperties } from "react";
2
+
3
+ import { useMindMapState } from "../../state/mindMap";
4
+ import { useShallow } from "zustand/react/shallow";
5
+ import { cn } from "../../lib/utils";
6
+
7
+ export interface BackgroundProps {
8
+ /** Classes CSS adicionais; quando passado, o fundo padrão (bg-slate-50) não é aplicado, permitindo estilização coerente com a aplicação */
9
+ className?: string;
10
+ /** Estilos inline; mesclado após os estilos internos (grid, offset), permitindo sobrescrever cor de fundo, grid etc. */
11
+ style?: CSSProperties;
12
+ }
13
+
14
+ const GRID_SIZE = 40;
15
+
16
+ export function Background({ className, style }: BackgroundProps = {}) {
17
+ const { offset, scale } = useMindMapState(
18
+ useShallow((state) => ({
19
+ offset: state.offset,
20
+ scale: state.scale,
21
+ })),
22
+ );
23
+ const gridSizePx = GRID_SIZE * scale;
24
+
25
+ return (
26
+ <div
27
+ className={cn("absolute inset-0", className ?? "bg-slate-50")}
28
+ style={
29
+ {
30
+ backgroundImage:
31
+ "radial-gradient(rgba(148, 163, 184, 0.45) 1px, transparent 1px)",
32
+ backgroundSize: `${gridSizePx}px ${gridSizePx}px`,
33
+ backgroundPosition: `${offset.x}px ${offset.y}px`,
34
+ ...style,
35
+ } as CSSProperties
36
+ }
37
+ />
38
+ );
39
+ }
@@ -0,0 +1,159 @@
1
+ import type { CSSProperties } from "react";
2
+ import { useRootMouseHandlers } from "../../hooks/mindMap/useRootMouseHandlers";
3
+ import { useMindMapHistoryDebounce } from "../../hooks/mindMap/useMindMapHistoryDebounce";
4
+ import { useUpdateCenter } from "../../hooks/mindMap/useUpdateCenter";
5
+ import { useMindMapState } from "../../state/mindMap";
6
+ import { type ReactNode, useEffect, useRef } from "react";
7
+ import { Nodes } from "./Nodes";
8
+ import {
9
+ NodeStylePopover,
10
+ type NodeStylePopoverStyleSlots,
11
+ } from "./NodeStylePopover";
12
+ import { KeyboardHelpDialog } from "./KeyboardHelpDialog";
13
+ import { cn } from "../../lib/utils";
14
+
15
+ /** Slots para estilizar o Board, nós, barra de edição e modal de ajuda */
16
+ export interface BoardStyleSlots extends NodeStylePopoverStyleSlots {
17
+ /** Raiz do board */
18
+ className?: string;
19
+ style?: CSSProperties;
20
+ /** Container dos nós (área com transform scale/offset) */
21
+ nodesWrapperClassName?: string;
22
+ nodesWrapperStyle?: CSSProperties;
23
+ /** Nó central */
24
+ centralNodeClassName?: string;
25
+ centralNodeStyle?: CSSProperties;
26
+ centralNodeContentClassName?: string;
27
+ centralNodeContentStyle?: CSSProperties;
28
+ /** Nó padrão (ramo) */
29
+ defaultNodeClassName?: string;
30
+ defaultNodeStyle?: CSSProperties;
31
+ defaultNodeContentClassName?: string;
32
+ defaultNodeContentStyle?: CSSProperties;
33
+ /** Nó de imagem */
34
+ imageNodeClassName?: string;
35
+ imageNodeStyle?: CSSProperties;
36
+ imageNodeContentClassName?: string;
37
+ imageNodeContentStyle?: CSSProperties;
38
+ /** Modal de ajuda (Alt+H): conteúdo do diálogo */
39
+ helpDialogContentClassName?: string;
40
+ helpDialogContentStyle?: CSSProperties;
41
+ /** Modal de ajuda: título */
42
+ helpDialogTitleClassName?: string;
43
+ /** Modal de ajuda: descrição */
44
+ helpDialogDescriptionClassName?: string;
45
+ /** Modal de ajuda: cada linha de atalho */
46
+ helpDialogItemClassName?: string;
47
+ /** Modal de ajuda: badge da tecla */
48
+ helpDialogShortcutKeyClassName?: string;
49
+ /** Modal de ajuda: texto da descrição do atalho */
50
+ helpDialogShortcutDescriptionClassName?: string;
51
+ }
52
+
53
+ export interface BoardProps extends BoardStyleSlots {
54
+ children?: ReactNode;
55
+ /** Optional array of colors applied by branch: each direct child of the central node gets a color, and all its descendants keep the same color. */
56
+ segmentColors?: string[];
57
+ }
58
+
59
+ export function Board({
60
+ children,
61
+ className,
62
+ style,
63
+ nodesWrapperClassName,
64
+ nodesWrapperStyle,
65
+ centralNodeClassName,
66
+ centralNodeStyle,
67
+ centralNodeContentClassName,
68
+ centralNodeContentStyle,
69
+ defaultNodeClassName,
70
+ defaultNodeStyle,
71
+ defaultNodeContentClassName,
72
+ defaultNodeContentStyle,
73
+ imageNodeClassName,
74
+ imageNodeStyle,
75
+ imageNodeContentClassName,
76
+ imageNodeContentStyle,
77
+ contentClassName: nodeStylePopoverContentClassName,
78
+ contentStyle: nodeStylePopoverContentStyle,
79
+ buttonClassName: nodeStylePopoverButtonClassName,
80
+ buttonStyle: nodeStylePopoverButtonStyle,
81
+ toggleItemClassName: nodeStylePopoverToggleItemClassName,
82
+ selectTriggerClassName: nodeStylePopoverSelectTriggerClassName,
83
+ selectContentClassName: nodeStylePopoverSelectContentClassName,
84
+ textColors: nodeStylePopoverTextColors,
85
+ backgroundColors: nodeStylePopoverBackgroundColors,
86
+ segmentColors,
87
+ helpDialogContentClassName,
88
+ helpDialogContentStyle,
89
+ helpDialogTitleClassName,
90
+ helpDialogDescriptionClassName,
91
+ helpDialogItemClassName,
92
+ helpDialogShortcutKeyClassName,
93
+ helpDialogShortcutDescriptionClassName,
94
+ }: BoardProps) {
95
+ const rootRef = useRef<HTMLDivElement | null>(null);
96
+ const applySegmentColors = useMindMapState((s) => s.applySegmentColors);
97
+ const nodes = useMindMapState((s) => s.nodes);
98
+
99
+ const { ...mouseHandlers } = useRootMouseHandlers({
100
+ rootRef,
101
+ });
102
+
103
+ useUpdateCenter({ rootRef });
104
+ useMindMapHistoryDebounce({ delayMs: 3000 });
105
+
106
+ useEffect(() => {
107
+ if (segmentColors?.length) {
108
+ applySegmentColors(segmentColors);
109
+ }
110
+ }, [segmentColors, applySegmentColors, nodes]);
111
+
112
+ return (
113
+ <div
114
+ data-nodex-root
115
+ className={cn("relative flex-1 overflow-hidden cursor-grab", className)}
116
+ style={style}
117
+ ref={rootRef}
118
+ {...mouseHandlers}
119
+ >
120
+ {children}
121
+ <KeyboardHelpDialog
122
+ contentClassName={helpDialogContentClassName}
123
+ contentStyle={helpDialogContentStyle}
124
+ titleClassName={helpDialogTitleClassName}
125
+ descriptionClassName={helpDialogDescriptionClassName}
126
+ itemClassName={helpDialogItemClassName}
127
+ shortcutKeyClassName={helpDialogShortcutKeyClassName}
128
+ shortcutDescriptionClassName={helpDialogShortcutDescriptionClassName}
129
+ />
130
+ <NodeStylePopover
131
+ contentClassName={nodeStylePopoverContentClassName}
132
+ contentStyle={nodeStylePopoverContentStyle}
133
+ buttonClassName={nodeStylePopoverButtonClassName}
134
+ buttonStyle={nodeStylePopoverButtonStyle}
135
+ toggleItemClassName={nodeStylePopoverToggleItemClassName}
136
+ selectTriggerClassName={nodeStylePopoverSelectTriggerClassName}
137
+ selectContentClassName={nodeStylePopoverSelectContentClassName}
138
+ textColors={nodeStylePopoverTextColors}
139
+ backgroundColors={nodeStylePopoverBackgroundColors}
140
+ />
141
+ <Nodes
142
+ nodesWrapperClassName={nodesWrapperClassName}
143
+ nodesWrapperStyle={nodesWrapperStyle}
144
+ centralNodeClassName={centralNodeClassName}
145
+ centralNodeStyle={centralNodeStyle}
146
+ centralNodeContentClassName={centralNodeContentClassName}
147
+ centralNodeContentStyle={centralNodeContentStyle}
148
+ defaultNodeClassName={defaultNodeClassName}
149
+ defaultNodeStyle={defaultNodeStyle}
150
+ defaultNodeContentClassName={defaultNodeContentClassName}
151
+ defaultNodeContentStyle={defaultNodeContentStyle}
152
+ imageNodeClassName={imageNodeClassName}
153
+ imageNodeStyle={imageNodeStyle}
154
+ imageNodeContentClassName={imageNodeContentClassName}
155
+ imageNodeContentStyle={imageNodeContentStyle}
156
+ />
157
+ </div>
158
+ );
159
+ }
@@ -0,0 +1,121 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { MindMapNode } from "../../state/mindMap";
3
+ import { useRef } from "react";
4
+ import { useMindMapState } from "../../state/mindMap";
5
+ import { useShallow } from "zustand/react/shallow";
6
+ import { useMindMapNodeMouseHandlers } from "../../hooks/mindMap/useMindMapNodeMouseHandlers";
7
+ import { useMindMapNode } from "../../hooks/mindMap/useMindMapNode";
8
+ import { useMindMapNodeEditor } from "../../hooks/mindMap/useMindMapNodeEditor";
9
+ import { cn } from "../../lib/utils";
10
+
11
+ interface CentalNodeProps {
12
+ node: MindMapNode;
13
+ className?: string;
14
+ style?: CSSProperties;
15
+ /** Caixa do conteúdo (círculo com texto) */
16
+ contentClassName?: string;
17
+ contentStyle?: CSSProperties;
18
+ }
19
+
20
+ export function CentalNode({
21
+ node,
22
+ className,
23
+ style,
24
+ contentClassName,
25
+ contentStyle,
26
+ }: CentalNodeProps) {
27
+ const { editingNodeId, selectedNodeId, readOnly } = useMindMapState(
28
+ useShallow((state) => ({
29
+ selectedNodeId: state.selectedNodeId,
30
+ editingNodeId: state.editingNodeId,
31
+ readOnly: state.readOnly,
32
+ })),
33
+ );
34
+
35
+ const textRef = useRef<HTMLSpanElement | null>(null);
36
+ const { node: logicalNode } = useMindMapNode({ nodeId: node.id });
37
+ const { onMouseDown, onDoubleClick } = useMindMapNodeMouseHandlers(node.id);
38
+ const editableHandlers = useMindMapNodeEditor({
39
+ nodeId: node.id,
40
+ text: node.text,
41
+ textRef,
42
+ });
43
+
44
+ return (
45
+ <div
46
+ className={cn("group absolute", className)}
47
+ data-nodex-node
48
+ style={{
49
+ transform: `translate(${node.pos.x}px, ${node.pos.y}px)`,
50
+ width: node.style.w + node.style.wrapperPadding * 2,
51
+ height: node.style.h + node.style.wrapperPadding * 2,
52
+ ...style,
53
+ }}
54
+ onMouseDown={onMouseDown}
55
+ onDoubleClick={onDoubleClick}
56
+ >
57
+ <div
58
+ className="relative h-full w-full"
59
+ style={{ padding: node.style.wrapperPadding }}
60
+ >
61
+ <div
62
+ data-bold={node.style.isBold}
63
+ data-italic={node.style.isItalic}
64
+ className={cn(
65
+ "flex items-center justify-center rounded-full border border-slate-300 bg-white text-slate-900 shadow-sm data-[bold=true]:font-bold data-[italic=true]:italic",
66
+ editingNodeId === node.id ? "select-text" : "select-none",
67
+ contentClassName,
68
+ )}
69
+ style={{
70
+ ...contentStyle,
71
+ width: node.style.w,
72
+ height: node.style.h,
73
+ padding: `${node.style.padding.y}px ${node.style.padding.x}px`,
74
+ borderColor: node.style.color,
75
+ fontSize: node.style.fontSize,
76
+ textAlign: node.style.textAlign,
77
+ boxShadow:
78
+ selectedNodeId === node.id
79
+ ? `0 0 0 3px ${node.style.color}`
80
+ : undefined,
81
+ color: node.style.textColor,
82
+ backgroundColor: node.style.backgroundColor,
83
+ }}
84
+ onDoubleClick={onDoubleClick}
85
+ >
86
+ <span
87
+ ref={textRef}
88
+ className="whitespace-pre outline-none leading-none"
89
+ contentEditable={!readOnly && editingNodeId === node.id}
90
+ suppressContentEditableWarning
91
+ onMouseDown={(event) => {
92
+ if (event.detail > 1) {
93
+ event.preventDefault();
94
+ }
95
+ }}
96
+ {...editableHandlers}
97
+ />
98
+ </div>
99
+ </div>
100
+
101
+ <button
102
+ type="button"
103
+ data-selected={selectedNodeId === node.id}
104
+ data-editing={editingNodeId === node.id}
105
+ data-read-only={readOnly}
106
+ className="absolute data-[read-only=false]:data-[selected=true]:data-[editing=false]:flex hidden -right-3 top-1/2 h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full border border-slate-300 bg-white text-sm font-bold text-slate-700 shadow-sm transition"
107
+ style={{ borderColor: node.style.color, color: node.style.color }}
108
+ onMouseDown={(event) => {
109
+ event.stopPropagation();
110
+ }}
111
+ onClick={(event) => {
112
+ event.stopPropagation();
113
+ logicalNode?.addChild();
114
+ }}
115
+ aria-label="Adicionar node"
116
+ >
117
+ +
118
+ </button>
119
+ </div>
120
+ );
121
+ }