@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.
- package/README.md +228 -0
- package/package.json +54 -0
- package/src/components/mindMap/Background.tsx +39 -0
- package/src/components/mindMap/Board.tsx +159 -0
- package/src/components/mindMap/CentalNode.tsx +121 -0
- package/src/components/mindMap/DefaultNode.tsx +205 -0
- package/src/components/mindMap/Header.tsx +247 -0
- package/src/components/mindMap/ImageNode.tsx +345 -0
- package/src/components/mindMap/KeyboardHelpDialog.tsx +108 -0
- package/src/components/mindMap/MineMap.tsx +237 -0
- package/src/components/mindMap/NodeStylePopover.tsx +486 -0
- package/src/components/mindMap/Nodes.tsx +113 -0
- package/src/components/mindMap/Nodex.tsx +65 -0
- package/src/components/mindMap/SaveStatusIndicator.tsx +61 -0
- package/src/components/mindMap/Segments.tsx +270 -0
- package/src/components/mindMap/ZenCard.tsx +41 -0
- package/src/components/ui/dialog.tsx +141 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/select.tsx +192 -0
- package/src/components/ui/toggle-group.tsx +83 -0
- package/src/components/ui/toggle.tsx +45 -0
- package/src/config/rootKeyBinds.ts +191 -0
- package/src/config/shortCuts.ts +28 -0
- package/src/contexts/MindMapNodeEditorContext.tsx +47 -0
- package/src/handlers/rootKeyBinds/handleAltEKeyBind.ts +6 -0
- package/src/handlers/rootKeyBinds/handleAltHKeyBind.ts +6 -0
- package/src/handlers/rootKeyBinds/handleAltWKeyBind.ts +6 -0
- package/src/handlers/rootKeyBinds/handleAltZKeyBind.ts +6 -0
- package/src/handlers/rootKeyBinds/handleArrowHorizontalRootKeyBind.ts +46 -0
- package/src/handlers/rootKeyBinds/handleArrowVerticalRootKeyBind.ts +44 -0
- package/src/handlers/rootKeyBinds/handleBackEspaceKeyBind.ts +12 -0
- package/src/handlers/rootKeyBinds/handleERootKeyBind.ts +16 -0
- package/src/handlers/rootKeyBinds/handleEnterRootKeyBind.ts +35 -0
- package/src/handlers/rootKeyBinds/handleEscapeKeyBind.ts +24 -0
- package/src/handlers/rootKeyBinds/handleEspaceKeyBind.ts +11 -0
- package/src/handlers/rootKeyBinds/handleMoveByWorldKeyBind.ts +6 -0
- package/src/handlers/rootKeyBinds/handleRedoRootKeyBind.ts +23 -0
- package/src/handlers/rootKeyBinds/handleTabRootKeyBind.ts +49 -0
- package/src/handlers/rootKeyBinds/handleTransformNodeKeyBind.ts +39 -0
- package/src/handlers/rootKeyBinds/handleUndoRootKeyBind.ts +23 -0
- package/src/handlers/rootKeyBinds/handleZoonByKeyBind.ts +31 -0
- package/src/helpers/centerNode.ts +19 -0
- package/src/helpers/getNodeSide.ts +16 -0
- package/src/hooks/mindMap/useHelpers.tsx +9 -0
- package/src/hooks/mindMap/useMindMapDebounce.ts +47 -0
- package/src/hooks/mindMap/useMindMapHistoryDebounce.ts +69 -0
- package/src/hooks/mindMap/useMindMapNode.tsx +203 -0
- package/src/hooks/mindMap/useMindMapNodeEditor.ts +91 -0
- package/src/hooks/mindMap/useMindMapNodeMouseHandlers.ts +24 -0
- package/src/hooks/mindMap/useRootKeyBindHandlers.ts +49 -0
- package/src/hooks/mindMap/useRootMouseHandlers.ts +124 -0
- package/src/hooks/mindMap/useUpdateCenter.ts +54 -0
- package/src/index.ts +76 -0
- package/src/lib/utils.ts +6 -0
- package/src/state/mindMap.ts +793 -0
- package/src/state/mindMapHistory.ts +96 -0
- package/src/styles.input.css +95 -0
- package/src/utils/exportMindMapAsHighQualityImage.ts +327 -0
- package/src/utils/exportMindMapAsMarkdown.ts +102 -0
- package/src/utils/exportMindMapAsPdf.ts +241 -0
- package/src/utils/getMindMapPreviewDataUrl.ts +60 -0
- package/styles.css +2 -0
|
@@ -0,0 +1,205 @@
|
|
|
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 DefaultNodeProps {
|
|
12
|
+
node: MindMapNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
/** Caixa do conteúdo (retângulo com texto) */
|
|
16
|
+
contentClassName?: string;
|
|
17
|
+
contentStyle?: CSSProperties;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function DefaultNode({
|
|
21
|
+
node,
|
|
22
|
+
className,
|
|
23
|
+
style,
|
|
24
|
+
contentClassName,
|
|
25
|
+
contentStyle,
|
|
26
|
+
}: DefaultNodeProps) {
|
|
27
|
+
const { node: logicalNode } = useMindMapNode({ nodeId: node.id });
|
|
28
|
+
|
|
29
|
+
if (!logicalNode) return;
|
|
30
|
+
|
|
31
|
+
const { editingNodeId, selectedNodeId, readOnly } = useMindMapState(
|
|
32
|
+
useShallow((state) => ({
|
|
33
|
+
selectedNodeId: state.selectedNodeId,
|
|
34
|
+
editingNodeId: state.editingNodeId,
|
|
35
|
+
readOnly: state.readOnly,
|
|
36
|
+
})),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const side = logicalNode.getSide();
|
|
40
|
+
const isLeft = side === "left";
|
|
41
|
+
|
|
42
|
+
const hasChildren = node.childrens.length > 0;
|
|
43
|
+
const childrenVisible = node.childrens.some((child) => child.isVisible);
|
|
44
|
+
const textRef = useRef<HTMLSpanElement | null>(null);
|
|
45
|
+
|
|
46
|
+
const editableHandlers = useMindMapNodeEditor({
|
|
47
|
+
nodeId: node.id,
|
|
48
|
+
text: node.text,
|
|
49
|
+
textRef,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const { onMouseDown, onDoubleClick } = useMindMapNodeMouseHandlers(node.id);
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<div
|
|
56
|
+
className={cn("group absolute", className)}
|
|
57
|
+
data-nodex-node
|
|
58
|
+
style={{
|
|
59
|
+
transform: `translate(${node.pos.x}px, ${node.pos.y}px)`,
|
|
60
|
+
width: node.style.w + node.style.wrapperPadding * 2,
|
|
61
|
+
height: node.style.h + node.style.wrapperPadding * 2,
|
|
62
|
+
...style,
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className="relative h-full w-full"
|
|
67
|
+
style={{ padding: node.style.wrapperPadding }}
|
|
68
|
+
>
|
|
69
|
+
<div
|
|
70
|
+
className={cn(
|
|
71
|
+
"flex items-center justify-center rounded-xl text-slate-900 data-[bold=true]:font-semibold data-[italic=true]:italic",
|
|
72
|
+
editingNodeId === node.id ? "select-text" : "select-none",
|
|
73
|
+
contentClassName,
|
|
74
|
+
)}
|
|
75
|
+
data-bold={node.style.isBold}
|
|
76
|
+
data-italic={node.style.isItalic}
|
|
77
|
+
style={{
|
|
78
|
+
...contentStyle,
|
|
79
|
+
width: node.style.w,
|
|
80
|
+
height: node.style.h,
|
|
81
|
+
padding: `${node.style.padding.y}px ${node.style.padding.x}px`,
|
|
82
|
+
borderColor: node.style.color,
|
|
83
|
+
fontSize: node.style.fontSize,
|
|
84
|
+
textAlign: node.style.textAlign,
|
|
85
|
+
boxShadow:
|
|
86
|
+
selectedNodeId === node.id
|
|
87
|
+
? `0 0 0 2px ${node.style.color}`
|
|
88
|
+
: undefined,
|
|
89
|
+
color: node.style.textColor,
|
|
90
|
+
backgroundColor: node.style.backgroundColor,
|
|
91
|
+
}}
|
|
92
|
+
onMouseDown={onMouseDown}
|
|
93
|
+
onDoubleClick={onDoubleClick}
|
|
94
|
+
>
|
|
95
|
+
<span
|
|
96
|
+
ref={textRef}
|
|
97
|
+
className="inline-block whitespace-pre outline-none leading-none"
|
|
98
|
+
contentEditable={!readOnly && editingNodeId === node.id}
|
|
99
|
+
suppressContentEditableWarning
|
|
100
|
+
onMouseDown={(event) => {
|
|
101
|
+
if (event.detail > 1) {
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
}
|
|
104
|
+
}}
|
|
105
|
+
{...editableHandlers}
|
|
106
|
+
/>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<span
|
|
110
|
+
aria-hidden="true"
|
|
111
|
+
data-left={isLeft}
|
|
112
|
+
data-selected={selectedNodeId === node.id}
|
|
113
|
+
className="pointer-events-none absolute top-1/2 h-[6px] -translate-y-1/2 rounded-md data-[left=false]:left-0 right-0 data-[selected=true]:hidden block"
|
|
114
|
+
style={{
|
|
115
|
+
width: node.style.wrapperPadding,
|
|
116
|
+
backgroundColor: node.style.color,
|
|
117
|
+
}}
|
|
118
|
+
/>
|
|
119
|
+
|
|
120
|
+
{hasChildren && (
|
|
121
|
+
<>
|
|
122
|
+
<span
|
|
123
|
+
aria-hidden="true"
|
|
124
|
+
className={`pointer-events-none absolute top-1/2 h-[6px] rounded-md -translate-y-1/2 ${
|
|
125
|
+
isLeft ? "left-0" : "right-0"
|
|
126
|
+
}`}
|
|
127
|
+
style={{
|
|
128
|
+
width: node.style.wrapperPadding / 2,
|
|
129
|
+
marginRight: isLeft ? 0 : 12,
|
|
130
|
+
marginLeft: isLeft ? 12 : 0,
|
|
131
|
+
backgroundColor: node.style.color,
|
|
132
|
+
}}
|
|
133
|
+
/>
|
|
134
|
+
|
|
135
|
+
<span
|
|
136
|
+
aria-hidden="true"
|
|
137
|
+
className={`absolute top-1/2 h-5 w-5 -translate-y-1/2 rounded-full text-xs flex items-center font-bold justify-center border-2 ${
|
|
138
|
+
isLeft ? "-left-1.5" : "-right-1.5"
|
|
139
|
+
}`}
|
|
140
|
+
style={{
|
|
141
|
+
color: node.style.color,
|
|
142
|
+
borderColor: node.style.color,
|
|
143
|
+
}}
|
|
144
|
+
onMouseDown={(event) => {
|
|
145
|
+
event.stopPropagation();
|
|
146
|
+
}}
|
|
147
|
+
onClick={(event) => {
|
|
148
|
+
event.stopPropagation();
|
|
149
|
+
logicalNode.togglechildrensVisibility();
|
|
150
|
+
}}
|
|
151
|
+
>
|
|
152
|
+
{!childrenVisible && node.childrens.length}
|
|
153
|
+
{childrenVisible && (
|
|
154
|
+
<span
|
|
155
|
+
className="w-3 h-3 rounded-full"
|
|
156
|
+
style={{
|
|
157
|
+
backgroundColor: node.style.color,
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</span>
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className={`absolute 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 ${
|
|
169
|
+
selectedNodeId === node.id && editingNodeId !== node.id && !readOnly
|
|
170
|
+
? "flex"
|
|
171
|
+
: "hidden"
|
|
172
|
+
} ${isLeft ? "-left-2.5" : "-right-2.5"}`}
|
|
173
|
+
style={{ borderColor: node.style.color, color: node.style.color }}
|
|
174
|
+
onMouseDown={(event) => {
|
|
175
|
+
event.stopPropagation();
|
|
176
|
+
}}
|
|
177
|
+
onClick={(event) => {
|
|
178
|
+
event.stopPropagation();
|
|
179
|
+
logicalNode.addChild();
|
|
180
|
+
}}
|
|
181
|
+
aria-label="Adicionar node"
|
|
182
|
+
>
|
|
183
|
+
+
|
|
184
|
+
</button>
|
|
185
|
+
|
|
186
|
+
<button
|
|
187
|
+
type="button"
|
|
188
|
+
className={`absolute 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-semibold text-slate-700 shadow-sm transition ${
|
|
189
|
+
selectedNodeId === node.id && !readOnly ? "flex" : "hidden"
|
|
190
|
+
} ${isLeft ? "-right-3" : "-left-3"}`}
|
|
191
|
+
style={{ borderColor: node.style.color, color: node.style.color }}
|
|
192
|
+
onMouseDown={(event) => {
|
|
193
|
+
event.stopPropagation();
|
|
194
|
+
}}
|
|
195
|
+
onClick={(event) => {
|
|
196
|
+
event.stopPropagation();
|
|
197
|
+
logicalNode.destroy();
|
|
198
|
+
}}
|
|
199
|
+
aria-label="Remover node"
|
|
200
|
+
>
|
|
201
|
+
X
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { useMindMapState } from "../../state/mindMap";
|
|
4
|
+
import { useShallow } from "zustand/react/shallow";
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
SaveStatusIndicator,
|
|
8
|
+
type MindMapSaveStatus,
|
|
9
|
+
} from "./SaveStatusIndicator";
|
|
10
|
+
import { exportMindMapAsHighQualityImage } from "../../utils/exportMindMapAsHighQualityImage";
|
|
11
|
+
import { exportMindMapAsMarkdown } from "../../utils/exportMindMapAsMarkdown";
|
|
12
|
+
import { exportMindMapAsPdf } from "../../utils/exportMindMapAsPdf";
|
|
13
|
+
import { Download, EllipsisVertical, FileText, LoaderCircle, FileDown } from "lucide-react";
|
|
14
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
|
|
15
|
+
|
|
16
|
+
/** Slots para estilizar cada parte interna do Header (ex.: integrar com tema da aplicação) */
|
|
17
|
+
export interface HeaderStyleSlots {
|
|
18
|
+
/** Raiz: elemento <header> */
|
|
19
|
+
className?: string;
|
|
20
|
+
style?: CSSProperties;
|
|
21
|
+
/** Container que envolve título e ações */
|
|
22
|
+
wrapperClassName?: string;
|
|
23
|
+
wrapperStyle?: CSSProperties;
|
|
24
|
+
/** Texto do título */
|
|
25
|
+
titleClassName?: string;
|
|
26
|
+
titleStyle?: CSSProperties;
|
|
27
|
+
/** Container dos botões (menu + indicador de save) */
|
|
28
|
+
actionsClassName?: string;
|
|
29
|
+
actionsStyle?: CSSProperties;
|
|
30
|
+
/** Botão do menu (três pontos) */
|
|
31
|
+
menuTriggerClassName?: string;
|
|
32
|
+
menuTriggerStyle?: CSSProperties;
|
|
33
|
+
/** Conteúdo do popover (dropdown de exportar) */
|
|
34
|
+
popoverContentClassName?: string;
|
|
35
|
+
popoverContentStyle?: CSSProperties;
|
|
36
|
+
/** Cada item do menu (Exportar imagem, MD, PDF) */
|
|
37
|
+
menuItemClassName?: string;
|
|
38
|
+
menuItemStyle?: CSSProperties;
|
|
39
|
+
/** Indicador de status de save */
|
|
40
|
+
saveStatusClassName?: string;
|
|
41
|
+
saveStatusStyle?: CSSProperties;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface HeaderProps extends HeaderStyleSlots {
|
|
45
|
+
title?: string;
|
|
46
|
+
saveStatus?: MindMapSaveStatus | null;
|
|
47
|
+
saveStatusLabels?: Partial<Record<MindMapSaveStatus, string>>;
|
|
48
|
+
/** When true, shows a menu (three dots) with export and future map options. */
|
|
49
|
+
showExportImageButton?: boolean;
|
|
50
|
+
/** Background color for the exported PNG image (e.g. "white", "#1e293b"). */
|
|
51
|
+
exportBackgroundColor?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Header({
|
|
55
|
+
title = "Nodex",
|
|
56
|
+
className,
|
|
57
|
+
style,
|
|
58
|
+
wrapperClassName,
|
|
59
|
+
wrapperStyle,
|
|
60
|
+
titleClassName,
|
|
61
|
+
titleStyle,
|
|
62
|
+
actionsClassName,
|
|
63
|
+
actionsStyle,
|
|
64
|
+
menuTriggerClassName,
|
|
65
|
+
menuTriggerStyle,
|
|
66
|
+
popoverContentClassName,
|
|
67
|
+
popoverContentStyle,
|
|
68
|
+
menuItemClassName,
|
|
69
|
+
menuItemStyle,
|
|
70
|
+
saveStatus = null,
|
|
71
|
+
saveStatusClassName,
|
|
72
|
+
saveStatusStyle,
|
|
73
|
+
saveStatusLabels,
|
|
74
|
+
showExportImageButton = false,
|
|
75
|
+
exportBackgroundColor,
|
|
76
|
+
}: HeaderProps) {
|
|
77
|
+
const [exporting, setExporting] = useState(false);
|
|
78
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
79
|
+
const { zenMode, nodes } = useMindMapState(
|
|
80
|
+
useShallow((state) => ({
|
|
81
|
+
zenMode: state.zenMode,
|
|
82
|
+
nodes: state.nodes,
|
|
83
|
+
getFlatNodes: state.getFlatNodes,
|
|
84
|
+
})),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const slug =
|
|
88
|
+
(title ?? "mind-map")
|
|
89
|
+
.toLowerCase()
|
|
90
|
+
.replace(/\s+/g, "-")
|
|
91
|
+
.replace(/[^a-z0-9-]/g, "") || `mind-map-${Date.now()}`;
|
|
92
|
+
|
|
93
|
+
const handleExportImage = async () => {
|
|
94
|
+
if (exporting || nodes.length === 0) return;
|
|
95
|
+
setExporting(true);
|
|
96
|
+
try {
|
|
97
|
+
await exportMindMapAsHighQualityImage(nodes, {
|
|
98
|
+
filename: slug,
|
|
99
|
+
exportBackgroundColor,
|
|
100
|
+
});
|
|
101
|
+
setMenuOpen(false);
|
|
102
|
+
} finally {
|
|
103
|
+
setExporting(false);
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const handleExportMarkdown = () => {
|
|
108
|
+
if (nodes.length === 0) return;
|
|
109
|
+
exportMindMapAsMarkdown(nodes, { filename: slug });
|
|
110
|
+
setMenuOpen(false);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const handleExportPdf = () => {
|
|
114
|
+
if (nodes.length === 0) return;
|
|
115
|
+
setExporting(true);
|
|
116
|
+
try {
|
|
117
|
+
exportMindMapAsPdf(nodes, { filename: slug });
|
|
118
|
+
setMenuOpen(false);
|
|
119
|
+
} finally {
|
|
120
|
+
setExporting(false);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<header
|
|
126
|
+
data-zen={zenMode}
|
|
127
|
+
className={cn(
|
|
128
|
+
"overflow-hidden border-b flex items-center w-full border-slate-200 bg-white px-4 py-3 text-base font-semibold transition-all duration-200 data-[zen=true]:pointer-events-none data-[zen=true]:max-h-0 data-[zen=true]:-translate-y-2 data-[zen=true]:opacity-0 max-h-16 translate-y-0 opacity-100",
|
|
129
|
+
className,
|
|
130
|
+
)}
|
|
131
|
+
style={style}
|
|
132
|
+
>
|
|
133
|
+
<div
|
|
134
|
+
className={cn("flex items-center justify-between gap-3 w-full", wrapperClassName)}
|
|
135
|
+
style={wrapperStyle}
|
|
136
|
+
>
|
|
137
|
+
<span className={cn("truncate", titleClassName)} style={titleStyle}>
|
|
138
|
+
{title}
|
|
139
|
+
</span>
|
|
140
|
+
|
|
141
|
+
<div
|
|
142
|
+
className={cn("flex shrink-0 items-center gap-2", actionsClassName)}
|
|
143
|
+
style={actionsStyle}
|
|
144
|
+
>
|
|
145
|
+
{showExportImageButton && (
|
|
146
|
+
<Popover open={menuOpen} onOpenChange={setMenuOpen}>
|
|
147
|
+
<PopoverTrigger asChild>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
disabled={nodes.length === 0}
|
|
151
|
+
className={cn(
|
|
152
|
+
"flex h-8 w-8 items-center justify-center rounded-md text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 disabled:opacity-50",
|
|
153
|
+
menuTriggerClassName,
|
|
154
|
+
)}
|
|
155
|
+
style={menuTriggerStyle}
|
|
156
|
+
title="Opções do mapa"
|
|
157
|
+
aria-label="Opções do mapa"
|
|
158
|
+
aria-expanded={menuOpen}
|
|
159
|
+
>
|
|
160
|
+
{exporting ? (
|
|
161
|
+
<LoaderCircle
|
|
162
|
+
className="h-4 w-4 animate-spin"
|
|
163
|
+
aria-hidden
|
|
164
|
+
/>
|
|
165
|
+
) : (
|
|
166
|
+
<EllipsisVertical className="h-4 w-4" aria-hidden />
|
|
167
|
+
)}
|
|
168
|
+
</button>
|
|
169
|
+
</PopoverTrigger>
|
|
170
|
+
<PopoverContent
|
|
171
|
+
align="end"
|
|
172
|
+
className={cn("w-52 p-1", popoverContentClassName)}
|
|
173
|
+
style={popoverContentStyle}
|
|
174
|
+
>
|
|
175
|
+
<button
|
|
176
|
+
type="button"
|
|
177
|
+
onClick={handleExportImage}
|
|
178
|
+
disabled={exporting || nodes.length === 0}
|
|
179
|
+
className={cn(
|
|
180
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs text-slate-700 transition hover:bg-slate-100 disabled:opacity-50",
|
|
181
|
+
menuItemClassName,
|
|
182
|
+
)}
|
|
183
|
+
style={menuItemStyle}
|
|
184
|
+
>
|
|
185
|
+
{exporting ? (
|
|
186
|
+
<LoaderCircle
|
|
187
|
+
className="h-4 w-4 shrink-0 animate-spin"
|
|
188
|
+
aria-hidden
|
|
189
|
+
/>
|
|
190
|
+
) : (
|
|
191
|
+
<Download className="h-4 w-4 shrink-0" aria-hidden />
|
|
192
|
+
)}
|
|
193
|
+
<span>
|
|
194
|
+
{exporting ? "Exportando…" : "Exportar imagem"}
|
|
195
|
+
</span>
|
|
196
|
+
</button>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
onClick={handleExportMarkdown}
|
|
200
|
+
disabled={nodes.length === 0}
|
|
201
|
+
className={cn(
|
|
202
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs text-slate-700 transition hover:bg-slate-100 disabled:opacity-50",
|
|
203
|
+
menuItemClassName,
|
|
204
|
+
)}
|
|
205
|
+
style={menuItemStyle}
|
|
206
|
+
>
|
|
207
|
+
<FileText className="h-4 w-4 shrink-0" aria-hidden />
|
|
208
|
+
<span>Exportar em texto (MD)</span>
|
|
209
|
+
</button>
|
|
210
|
+
<button
|
|
211
|
+
type="button"
|
|
212
|
+
onClick={handleExportPdf}
|
|
213
|
+
disabled={exporting || nodes.length === 0}
|
|
214
|
+
className={cn(
|
|
215
|
+
"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs text-slate-700 transition hover:bg-slate-100 disabled:opacity-50",
|
|
216
|
+
menuItemClassName,
|
|
217
|
+
)}
|
|
218
|
+
style={menuItemStyle}
|
|
219
|
+
>
|
|
220
|
+
<FileDown className="h-4 w-4 shrink-0" aria-hidden />
|
|
221
|
+
<span>Exportar PDF</span>
|
|
222
|
+
</button>
|
|
223
|
+
</PopoverContent>
|
|
224
|
+
</Popover>
|
|
225
|
+
)}
|
|
226
|
+
{saveStatus ? (
|
|
227
|
+
saveStatusStyle != null ? (
|
|
228
|
+
<span style={saveStatusStyle}>
|
|
229
|
+
<SaveStatusIndicator
|
|
230
|
+
status={saveStatus}
|
|
231
|
+
className={saveStatusClassName}
|
|
232
|
+
labels={saveStatusLabels}
|
|
233
|
+
/>
|
|
234
|
+
</span>
|
|
235
|
+
) : (
|
|
236
|
+
<SaveStatusIndicator
|
|
237
|
+
status={saveStatus}
|
|
238
|
+
className={saveStatusClassName}
|
|
239
|
+
labels={saveStatusLabels}
|
|
240
|
+
/>
|
|
241
|
+
)
|
|
242
|
+
) : null}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</header>
|
|
246
|
+
);
|
|
247
|
+
}
|