@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,61 @@
|
|
|
1
|
+
import { CheckCircle2, CircleAlert, LoaderCircle } from "lucide-react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
export type MindMapSaveStatus = "saved" | "unsaved" | "saving";
|
|
5
|
+
|
|
6
|
+
interface SaveStatusIndicatorProps {
|
|
7
|
+
status: MindMapSaveStatus;
|
|
8
|
+
className?: string;
|
|
9
|
+
labels?: Partial<Record<MindMapSaveStatus, string>>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const defaultLabels: Record<MindMapSaveStatus, string> = {
|
|
13
|
+
saved: "Salvo",
|
|
14
|
+
unsaved: "Não salvo",
|
|
15
|
+
saving: "Salvando...",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function SaveStatusIndicator({
|
|
19
|
+
status,
|
|
20
|
+
className,
|
|
21
|
+
labels,
|
|
22
|
+
}: SaveStatusIndicatorProps) {
|
|
23
|
+
const mergedLabels = {
|
|
24
|
+
...defaultLabels,
|
|
25
|
+
...labels,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (status === "saving") {
|
|
29
|
+
return (
|
|
30
|
+
<span
|
|
31
|
+
aria-label={mergedLabels.saving}
|
|
32
|
+
title={mergedLabels.saving}
|
|
33
|
+
className={cn("inline-flex items-center justify-center", className)}
|
|
34
|
+
>
|
|
35
|
+
<LoaderCircle className="size-4 animate-spin text-sky-600" />
|
|
36
|
+
</span>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (status === "unsaved") {
|
|
41
|
+
return (
|
|
42
|
+
<span
|
|
43
|
+
aria-label={mergedLabels.unsaved}
|
|
44
|
+
title={mergedLabels.unsaved}
|
|
45
|
+
className={cn("inline-flex items-center justify-center", className)}
|
|
46
|
+
>
|
|
47
|
+
<CircleAlert className="size-4 text-amber-600" />
|
|
48
|
+
</span>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<span
|
|
54
|
+
aria-label={mergedLabels.saved}
|
|
55
|
+
title={mergedLabels.saved}
|
|
56
|
+
className={cn("inline-flex items-center justify-center", className)}
|
|
57
|
+
>
|
|
58
|
+
<CheckCircle2 className="size-4 text-emerald-600" />
|
|
59
|
+
</span>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { MindMapNode } from "../../state/mindMap";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
|
|
5
|
+
interface SegmentsProps {
|
|
6
|
+
nodes: MindMapNode[];
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Point = {
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type SegmentLine = {
|
|
16
|
+
key: string;
|
|
17
|
+
start: Point;
|
|
18
|
+
end: Point;
|
|
19
|
+
controlStart: Point;
|
|
20
|
+
controlEnd: Point;
|
|
21
|
+
color: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const TOGGLE_EDGE_OUTSIDE_OFFSET = 6;
|
|
25
|
+
|
|
26
|
+
export function getNodeWrapper(node: MindMapNode) {
|
|
27
|
+
const wrapperPadding = node.style.wrapperPadding;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
left: node.pos.x,
|
|
31
|
+
top: node.pos.y,
|
|
32
|
+
width: node.style.w + wrapperPadding * 2,
|
|
33
|
+
height: node.style.h + wrapperPadding * 2,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getNodeCenter(node: MindMapNode): Point {
|
|
38
|
+
const wrapper = getNodeWrapper(node);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
x: wrapper.left + wrapper.width / 2,
|
|
42
|
+
y: wrapper.top + wrapper.height / 2,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getNodeAnchor(node: MindMapNode, side: "left" | "right"): Point {
|
|
47
|
+
const wrapper = getNodeWrapper(node);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
x: side === "right" ? wrapper.left + wrapper.width : wrapper.left,
|
|
51
|
+
y: wrapper.top + wrapper.height / 2,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function findCentralNode(nodes: MindMapNode[]): MindMapNode | null {
|
|
56
|
+
let centralNode: MindMapNode | null = null;
|
|
57
|
+
|
|
58
|
+
const walk = (node: MindMapNode) => {
|
|
59
|
+
if (centralNode) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (node.type === "central") {
|
|
64
|
+
centralNode = node;
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const child of node.childrens) {
|
|
69
|
+
walk(child);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const node of nodes) {
|
|
74
|
+
walk(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return centralNode;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function getBranchSide(
|
|
81
|
+
node: MindMapNode,
|
|
82
|
+
centralCenterX: number | null,
|
|
83
|
+
): "left" | "right" | null {
|
|
84
|
+
if (centralCenterX === null || node.type === "central") {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return getNodeCenter(node).x < centralCenterX ? "left" : "right";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getOutputAnchor(node: MindMapNode, side: "left" | "right"): Point {
|
|
92
|
+
if (node.type === "central") {
|
|
93
|
+
return {
|
|
94
|
+
x:
|
|
95
|
+
node.pos.x +
|
|
96
|
+
node.style.wrapperPadding +
|
|
97
|
+
(side === "right" ? node.style.w : 0),
|
|
98
|
+
y: node.pos.y + node.style.wrapperPadding + node.style.h / 2,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const wrapper = getNodeWrapper(node);
|
|
103
|
+
const hasChildren = node.childrens.length > 0;
|
|
104
|
+
|
|
105
|
+
if (!hasChildren) {
|
|
106
|
+
return getNodeAnchor(node, side);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
x:
|
|
111
|
+
side === "right"
|
|
112
|
+
? wrapper.left + wrapper.width + TOGGLE_EDGE_OUTSIDE_OFFSET
|
|
113
|
+
: wrapper.left - TOGGLE_EDGE_OUTSIDE_OFFSET,
|
|
114
|
+
y: wrapper.top + wrapper.height / 2,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getInputAnchor(node: MindMapNode, side: "left" | "right"): Point {
|
|
119
|
+
if (node.type === "central") {
|
|
120
|
+
return {
|
|
121
|
+
x:
|
|
122
|
+
node.pos.x +
|
|
123
|
+
node.style.wrapperPadding +
|
|
124
|
+
(side === "right" ? 0 : node.style.w),
|
|
125
|
+
y: node.pos.y + node.style.wrapperPadding + node.style.h / 2,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const wrapper = getNodeWrapper(node);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
x: side === "right" ? wrapper.left + 1 : wrapper.left + wrapper.width - 1,
|
|
133
|
+
y: wrapper.top + wrapper.height / 2 + 0.5,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function buildSegmentLine(
|
|
138
|
+
fromNode: MindMapNode,
|
|
139
|
+
toNode: MindMapNode,
|
|
140
|
+
centralCenterX: number | null,
|
|
141
|
+
): SegmentLine | null {
|
|
142
|
+
const fromCenter = getNodeCenter(fromNode);
|
|
143
|
+
const toCenter = getNodeCenter(toNode);
|
|
144
|
+
const branchSide =
|
|
145
|
+
getBranchSide(toNode, centralCenterX) ??
|
|
146
|
+
(toCenter.x >= fromCenter.x ? "right" : "left");
|
|
147
|
+
const oppositeSide = branchSide === "right" ? "left" : "right";
|
|
148
|
+
|
|
149
|
+
const start = getOutputAnchor(fromNode, branchSide);
|
|
150
|
+
const end = getInputAnchor(toNode, branchSide);
|
|
151
|
+
|
|
152
|
+
const dx = end.x - start.x;
|
|
153
|
+
const dy = end.y - start.y;
|
|
154
|
+
const length = Math.hypot(dx, dy);
|
|
155
|
+
|
|
156
|
+
if (length === 0) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const horizontalDistance = Math.abs(dx);
|
|
161
|
+
const verticalDistance = Math.abs(dy);
|
|
162
|
+
const direction = dx >= 0 ? 1 : -1;
|
|
163
|
+
const baseHandle = Math.min(120, Math.max(32, horizontalDistance * 0.45));
|
|
164
|
+
const verticalAdjustment = Math.min(28, verticalDistance * 0.2);
|
|
165
|
+
const handleSize = baseHandle + verticalAdjustment;
|
|
166
|
+
|
|
167
|
+
const controlStart = {
|
|
168
|
+
x: start.x + direction * handleSize,
|
|
169
|
+
y: start.y,
|
|
170
|
+
};
|
|
171
|
+
const controlEnd = {
|
|
172
|
+
x: end.x + (oppositeSide === "right" ? handleSize : -handleSize),
|
|
173
|
+
y: end.y,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
key: `${fromNode.id}-${toNode.id}`,
|
|
178
|
+
start,
|
|
179
|
+
end,
|
|
180
|
+
controlStart,
|
|
181
|
+
controlEnd,
|
|
182
|
+
color: toNode.style.color ?? fromNode.style.color ?? "#94a3b8",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function collectVisibleEdges(
|
|
187
|
+
parent: MindMapNode,
|
|
188
|
+
edges: Array<{ from: MindMapNode; to: MindMapNode }>,
|
|
189
|
+
) {
|
|
190
|
+
if (!parent.isVisible) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const child of parent.childrens) {
|
|
195
|
+
if (!child.isVisible) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
edges.push({ from: parent, to: child });
|
|
200
|
+
collectVisibleEdges(child, edges);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function getSegmentLines(nodes: MindMapNode[]): SegmentLine[] {
|
|
205
|
+
const edges: Array<{ from: MindMapNode; to: MindMapNode }> = [];
|
|
206
|
+
const centralNode = findCentralNode(nodes);
|
|
207
|
+
const centralCenterX = centralNode ? getNodeCenter(centralNode).x : null;
|
|
208
|
+
|
|
209
|
+
for (const node of nodes) {
|
|
210
|
+
collectVisibleEdges(node, edges);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return edges
|
|
214
|
+
.map(({ from, to }) => buildSegmentLine(from, to, centralCenterX))
|
|
215
|
+
.filter((segment): segment is SegmentLine => Boolean(segment));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getNodesBounds(nodes: MindMapNode[]): {
|
|
219
|
+
minX: number;
|
|
220
|
+
minY: number;
|
|
221
|
+
maxX: number;
|
|
222
|
+
maxY: number;
|
|
223
|
+
} | null {
|
|
224
|
+
let minX = Infinity;
|
|
225
|
+
let minY = Infinity;
|
|
226
|
+
let maxX = -Infinity;
|
|
227
|
+
let maxY = -Infinity;
|
|
228
|
+
let hasVisible = false;
|
|
229
|
+
|
|
230
|
+
const walk = (node: MindMapNode) => {
|
|
231
|
+
if (!node.isVisible) return;
|
|
232
|
+
hasVisible = true;
|
|
233
|
+
const w = getNodeWrapper(node);
|
|
234
|
+
minX = Math.min(minX, w.left);
|
|
235
|
+
minY = Math.min(minY, w.top);
|
|
236
|
+
maxX = Math.max(maxX, w.left + w.width);
|
|
237
|
+
maxY = Math.max(maxY, w.top + w.height);
|
|
238
|
+
node.childrens.forEach(walk);
|
|
239
|
+
};
|
|
240
|
+
nodes.forEach(walk);
|
|
241
|
+
|
|
242
|
+
if (!hasVisible) return null;
|
|
243
|
+
return { minX, minY, maxX, maxY };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function Segments({ nodes, className }: SegmentsProps) {
|
|
247
|
+
const segmentLines = useMemo(() => getSegmentLines(nodes), [nodes]);
|
|
248
|
+
|
|
249
|
+
return (
|
|
250
|
+
<svg
|
|
251
|
+
className={cn(
|
|
252
|
+
"absolute left-0 top-0 h-full w-full pointer-events-none overflow-visible",
|
|
253
|
+
className,
|
|
254
|
+
)}
|
|
255
|
+
aria-hidden="true"
|
|
256
|
+
style={{ overflow: "visible" }}
|
|
257
|
+
>
|
|
258
|
+
{segmentLines.map((segment) => (
|
|
259
|
+
<path
|
|
260
|
+
key={segment.key}
|
|
261
|
+
d={`M ${segment.start.x} ${segment.start.y} C ${segment.controlStart.x} ${segment.controlStart.y}, ${segment.controlEnd.x} ${segment.controlEnd.y}, ${segment.end.x} ${segment.end.y}`}
|
|
262
|
+
fill="none"
|
|
263
|
+
stroke={segment.color}
|
|
264
|
+
strokeWidth="6"
|
|
265
|
+
strokeLinecap="round"
|
|
266
|
+
/>
|
|
267
|
+
))}
|
|
268
|
+
</svg>
|
|
269
|
+
);
|
|
270
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { useMindMapState } from "../../state/mindMap";
|
|
3
|
+
import { useShallow } from "zustand/react/shallow";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
|
|
6
|
+
export interface ZenCardProps {
|
|
7
|
+
className?: string;
|
|
8
|
+
style?: CSSProperties;
|
|
9
|
+
/** Class for the subtitle line (e.g. "Alt+Z to exit") */
|
|
10
|
+
subtitleClassName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ZenCard({
|
|
14
|
+
className,
|
|
15
|
+
style,
|
|
16
|
+
subtitleClassName,
|
|
17
|
+
}: ZenCardProps = {}) {
|
|
18
|
+
const { zenMode } = useMindMapState(
|
|
19
|
+
useShallow((state) => ({ zenMode: state.zenMode })),
|
|
20
|
+
);
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
data-zen={zenMode}
|
|
24
|
+
className={cn(
|
|
25
|
+
"pointer-events-none absolute bottom-4 right-4 rounded-md border border-slate-200 bg-white/40 px-2 py-1 text-[10px] font-semibold uppercase tracking-wide text-slate-500 shadow-sm backdrop-blur transition-all duration-200 data-[zen=true]:opacity-100 data-[zen=true]:translate-y-0 opacity-0 translate-y-2",
|
|
26
|
+
className,
|
|
27
|
+
)}
|
|
28
|
+
style={style}
|
|
29
|
+
>
|
|
30
|
+
<div>Zen mode</div>
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"text-[9px] font-medium normal-case tracking-normal text-slate-400",
|
|
34
|
+
subtitleClassName,
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
Alt+Z to exit
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
|
3
|
+
import { XIcon } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils";
|
|
6
|
+
|
|
7
|
+
function Dialog({
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
|
10
|
+
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function DialogTrigger({
|
|
14
|
+
...props
|
|
15
|
+
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
|
16
|
+
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function DialogPortal({
|
|
20
|
+
...props
|
|
21
|
+
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
|
22
|
+
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function DialogClose({
|
|
26
|
+
...props
|
|
27
|
+
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
|
28
|
+
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function DialogOverlay({
|
|
32
|
+
className,
|
|
33
|
+
...props
|
|
34
|
+
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
|
35
|
+
return (
|
|
36
|
+
<DialogPrimitive.Overlay
|
|
37
|
+
data-slot="dialog-overlay"
|
|
38
|
+
className={cn(
|
|
39
|
+
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
/>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function DialogContent({
|
|
48
|
+
className,
|
|
49
|
+
children,
|
|
50
|
+
showCloseButton = true,
|
|
51
|
+
...props
|
|
52
|
+
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
53
|
+
showCloseButton?: boolean;
|
|
54
|
+
}) {
|
|
55
|
+
return (
|
|
56
|
+
<DialogPortal data-slot="dialog-portal">
|
|
57
|
+
<DialogOverlay />
|
|
58
|
+
<DialogPrimitive.Content
|
|
59
|
+
data-slot="dialog-content"
|
|
60
|
+
className={cn(
|
|
61
|
+
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
|
62
|
+
className,
|
|
63
|
+
)}
|
|
64
|
+
{...props}
|
|
65
|
+
>
|
|
66
|
+
{children}
|
|
67
|
+
{showCloseButton && (
|
|
68
|
+
<DialogPrimitive.Close
|
|
69
|
+
data-slot="dialog-close"
|
|
70
|
+
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
|
71
|
+
>
|
|
72
|
+
<XIcon />
|
|
73
|
+
<span className="sr-only">Close</span>
|
|
74
|
+
</DialogPrimitive.Close>
|
|
75
|
+
)}
|
|
76
|
+
</DialogPrimitive.Content>
|
|
77
|
+
</DialogPortal>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
|
82
|
+
return (
|
|
83
|
+
<div
|
|
84
|
+
data-slot="dialog-header"
|
|
85
|
+
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
92
|
+
return (
|
|
93
|
+
<div
|
|
94
|
+
data-slot="dialog-footer"
|
|
95
|
+
className={cn(
|
|
96
|
+
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
|
97
|
+
className,
|
|
98
|
+
)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function DialogTitle({
|
|
105
|
+
className,
|
|
106
|
+
...props
|
|
107
|
+
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
|
108
|
+
return (
|
|
109
|
+
<DialogPrimitive.Title
|
|
110
|
+
data-slot="dialog-title"
|
|
111
|
+
className={cn("text-lg leading-none font-semibold", className)}
|
|
112
|
+
{...props}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function DialogDescription({
|
|
118
|
+
className,
|
|
119
|
+
...props
|
|
120
|
+
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
|
121
|
+
return (
|
|
122
|
+
<DialogPrimitive.Description
|
|
123
|
+
data-slot="dialog-description"
|
|
124
|
+
className={cn("text-muted-foreground text-sm", className)}
|
|
125
|
+
{...props}
|
|
126
|
+
/>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export {
|
|
131
|
+
Dialog,
|
|
132
|
+
DialogClose,
|
|
133
|
+
DialogContent,
|
|
134
|
+
DialogDescription,
|
|
135
|
+
DialogFooter,
|
|
136
|
+
DialogHeader,
|
|
137
|
+
DialogOverlay,
|
|
138
|
+
DialogPortal,
|
|
139
|
+
DialogTitle,
|
|
140
|
+
DialogTrigger,
|
|
141
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
|
3
|
+
|
|
4
|
+
import { cn } from "../../lib/utils"
|
|
5
|
+
|
|
6
|
+
function Popover({
|
|
7
|
+
...props
|
|
8
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
|
9
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function PopoverTrigger({
|
|
13
|
+
...props
|
|
14
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
|
15
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function PopoverContent({
|
|
19
|
+
className,
|
|
20
|
+
align = "center",
|
|
21
|
+
sideOffset = 4,
|
|
22
|
+
...props
|
|
23
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
|
24
|
+
return (
|
|
25
|
+
<PopoverPrimitive.Portal>
|
|
26
|
+
<PopoverPrimitive.Content
|
|
27
|
+
data-slot="popover-content"
|
|
28
|
+
align={align}
|
|
29
|
+
sideOffset={sideOffset}
|
|
30
|
+
className={cn(
|
|
31
|
+
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
|
32
|
+
className
|
|
33
|
+
)}
|
|
34
|
+
{...props}
|
|
35
|
+
/>
|
|
36
|
+
</PopoverPrimitive.Portal>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function PopoverAnchor({
|
|
41
|
+
...props
|
|
42
|
+
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
|
43
|
+
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|