Skip to content

Commit 3699ece

Browse files
committed
feat:
1 parent 19635b3 commit 3699ece

7 files changed

Lines changed: 773 additions & 46 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"use client";
2+
3+
import { ZoomIn, ZoomOut, Maximize, Undo2, Redo2 } from "lucide-react";
4+
import { useReactFlow } from "@xyflow/react";
5+
import { useUndo, useRedo, useCanUndo, useCanRedo } from "@liveblocks/react";
6+
7+
// ---------------------------------------------------------------------------
8+
// CanvasControlBar
9+
// ---------------------------------------------------------------------------
10+
11+
/**
12+
* Floating pill toolbar at bottom-left of the canvas.
13+
* Contains:
14+
* - Group 1: Zoom Out, Fit View, Zoom In (via useReactFlow)
15+
* - Group 2: Undo, Redo (via Liveblocks history)
16+
*
17+
* Must be rendered inside both a ReactFlowProvider and a Liveblocks RoomProvider.
18+
*/
19+
export function CanvasControlBar() {
20+
const { zoomIn, zoomOut, fitView } = useReactFlow();
21+
const undo = useUndo();
22+
const redo = useRedo();
23+
const canUndo = useCanUndo();
24+
const canRedo = useCanRedo();
25+
26+
return (
27+
<div
28+
style={{
29+
position: "absolute",
30+
bottom: 24,
31+
left: 24,
32+
zIndex: 10,
33+
display: "flex",
34+
alignItems: "center",
35+
gap: 0,
36+
background: "var(--bg-surface)",
37+
border: "1px solid var(--border-default)",
38+
borderRadius: 9999,
39+
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
40+
padding: "4px 6px",
41+
}}
42+
>
43+
{/* ── Group 1: Zoom controls ─────────────────────────────────────────── */}
44+
<ControlButton
45+
label="Zoom out"
46+
onClick={() => zoomOut({ duration: 300 })}
47+
>
48+
<ZoomOut className="h-4 w-4" />
49+
</ControlButton>
50+
51+
<ControlButton
52+
label="Fit view"
53+
onClick={() => fitView({ duration: 300 })}
54+
>
55+
<Maximize className="h-4 w-4" />
56+
</ControlButton>
57+
58+
<ControlButton
59+
label="Zoom in"
60+
onClick={() => zoomIn({ duration: 300 })}
61+
>
62+
<ZoomIn className="h-4 w-4" />
63+
</ControlButton>
64+
65+
{/* ── Divider ────────────────────────────────────────────────────────── */}
66+
<div
67+
aria-hidden
68+
style={{
69+
width: 1,
70+
height: 16,
71+
background: "var(--border-default)",
72+
margin: "0 6px",
73+
flexShrink: 0,
74+
}}
75+
/>
76+
77+
{/* ── Group 2: History controls ──────────────────────────────────────── */}
78+
<ControlButton
79+
label="Undo"
80+
onClick={undo}
81+
disabled={!canUndo}
82+
>
83+
<Undo2 className="h-4 w-4" />
84+
</ControlButton>
85+
86+
<ControlButton
87+
label="Redo"
88+
onClick={redo}
89+
disabled={!canRedo}
90+
>
91+
<Redo2 className="h-4 w-4" />
92+
</ControlButton>
93+
</div>
94+
);
95+
}
96+
97+
// ---------------------------------------------------------------------------
98+
// Shared button primitive
99+
// ---------------------------------------------------------------------------
100+
101+
interface ControlButtonProps {
102+
label: string;
103+
onClick: () => void;
104+
disabled?: boolean;
105+
children: React.ReactNode;
106+
}
107+
108+
function ControlButton({ label, onClick, disabled = false, children }: ControlButtonProps) {
109+
return (
110+
<button
111+
aria-label={label}
112+
title={label}
113+
onClick={onClick}
114+
disabled={disabled}
115+
style={{
116+
display: "flex",
117+
alignItems: "center",
118+
justifyContent: "center",
119+
width: 32,
120+
height: 32,
121+
background: "transparent",
122+
border: "none",
123+
borderRadius: 9999,
124+
color: disabled ? "var(--text-muted)" : "var(--text-primary)",
125+
cursor: disabled ? "not-allowed" : "pointer",
126+
opacity: disabled ? 0.4 : 1,
127+
pointerEvents: disabled ? "none" : "auto",
128+
transition: "background 0.15s ease, color 0.15s ease",
129+
}}
130+
onMouseEnter={(e) => {
131+
if (!disabled) {
132+
(e.currentTarget as HTMLButtonElement).style.background =
133+
"color-mix(in srgb, var(--accent-primary) 10%, transparent)";
134+
(e.currentTarget as HTMLButtonElement).style.color = "var(--text-primary)";
135+
}
136+
}}
137+
onMouseLeave={(e) => {
138+
(e.currentTarget as HTMLButtonElement).style.background = "transparent";
139+
(e.currentTarget as HTMLButtonElement).style.color = disabled
140+
? "var(--text-muted)"
141+
: "var(--text-primary)";
142+
}}
143+
>
144+
{children}
145+
</button>
146+
);
147+
}

0 commit comments

Comments
 (0)