feat: add interactive viewport controls and refactor floating button styles

- Introduced `ViewportControls` for zoom, fit view, and interactive toggle in canvas lab.
- Extracted and reused `CANVAS_FLOATING_ICON_BUTTON_CLASS` for consistent button styling.
- Updated API base paths and server proxy settings.
- Enabled dynamic interaction states for nodes and connections in canvas lab.
This commit is contained in:
Shine1i 2026-02-12 00:18:56 +01:00
commit 49abc27533
6 changed files with 107 additions and 10 deletions

View file

@ -26,3 +26,4 @@ test/
*.njsproj
*.sln
*.sw?
/src/features/canvas-lab/AGENTS.md

View file

@ -1,4 +1,4 @@
const DEFAULT_BASE = "http://127.0.0.1:8000";
const DEFAULT_BASE = "/preview-api";
export const CANVAS_LAB_API_BASE =
import.meta.env.VITE_DATA_DESIGNER_API ?? DEFAULT_BASE;

View file

@ -1,7 +1,6 @@
import {
Background,
BackgroundVariant,
Controls,
type Edge,
type EdgeChange,
type EdgeTypes,
@ -26,9 +25,11 @@ import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import { EyeIcon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { Lock, LockOpen, Maximize2, Minus, Plus } from "lucide-react";
import { previewCanvas } from "./api";
import { CanvasAuxNode, type CanvasAuxNodeData } from "./components/canvas-aux-node";
import { BlockSheet } from "./components/block-sheet";
import { CANVAS_FLOATING_ICON_BUTTON_CLASS } from "./components/floating-icon-button-class";
import { CanvasNode } from "./components/canvas-node";
import { CanvasSemanticEdge } from "./components/canvas-semantic-edge";
import { DataEdge } from "./components/rf-ui/data-edge";
@ -56,6 +57,11 @@ type LayoutControlsProps = {
onToggleDirection: () => void;
};
type ViewportControlsProps = {
interactive: boolean;
onToggleInteractive: () => void;
};
type InternalsSyncProps = {
nodeIds: string[];
};
@ -125,6 +131,74 @@ function LayoutControls({
);
}
function ViewportControls({
interactive,
onToggleInteractive,
}: ViewportControlsProps): ReactElement {
const { zoomIn, zoomOut, fitView } = useReactFlow();
const handleZoomIn = useCallback(() => {
zoomIn({ duration: 150 });
}, [zoomIn]);
const handleZoomOut = useCallback(() => {
zoomOut({ duration: 150 });
}, [zoomOut]);
const handleFitView = useCallback(() => {
fitView({ duration: 250 });
}, [fitView]);
return (
<Panel position="bottom-left" className="m-3 flex items-center gap-2">
<Button
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={handleZoomIn}
aria-label="Zoom in"
>
<Plus className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={handleZoomOut}
aria-label="Zoom out"
>
<Minus className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={handleFitView}
aria-label="Fit view"
>
<Maximize2 className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={onToggleInteractive}
aria-label={interactive ? "Lock interaction" : "Unlock interaction"}
>
{interactive ? (
<LockOpen className="size-4" />
) : (
<Lock className="size-4" />
)}
</Button>
</Panel>
);
}
export function CanvasLabPage(): ReactElement {
const {
nodes,
@ -200,6 +274,7 @@ export function CanvasLabPage(): ReactElement {
const [copied, setCopied] = useState(false);
const [importOpen, setImportOpen] = useState(false);
const [processorsOpen, setProcessorsOpen] = useState(false);
const [interactive, setInteractive] = useState(true);
const [statusMessage, setStatusMessage] = useState<{
tone: "success" | "error";
text: string;
@ -481,6 +556,9 @@ export function CanvasLabPage(): ReactElement {
onConnect={onConnect}
onNodeClick={handleNodeClick}
isValidConnection={isValidConnection}
nodesDraggable={interactive}
nodesConnectable={interactive}
elementsSelectable={interactive}
fitView={true}
className="h-full w-full"
>
@ -512,7 +590,10 @@ export function CanvasLabPage(): ReactElement {
onImport={() => setImportOpen(true)}
/>
</Panel>
<Controls position="bottom-left" />
<ViewportControls
interactive={interactive}
onToggleInteractive={() => setInteractive((value) => !value)}
/>
</ReactFlow>
</div>
</main>

View file

@ -18,6 +18,7 @@ import {
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import type { ReactElement } from "react";
import { CANVAS_FLOATING_ICON_BUTTON_CLASS } from "./floating-icon-button-class";
import type { LlmType, SamplerType } from "../types";
import { BLOCK_GROUPS, getBlocksForKind } from "../blocks/registry";
@ -118,9 +119,6 @@ function BlockSheetButton({
);
}
const FLOATING_ICON_BUTTON_CLASS =
"corner-squircle group h-11 w-11 rounded-xl border border-border/60 bg-transparent p-0 text-muted-foreground hover:bg-transparent hover:text-primary hover:border-primary/60";
export function BlockSheet({
container,
view,
@ -146,7 +144,11 @@ export function BlockSheet({
}}
>
<SheetTrigger asChild={true}>
<Button size="icon" className={FLOATING_ICON_BUTTON_CLASS} variant="ghost">
<Button
size="icon"
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
variant="ghost"
>
<HugeiconsIcon
icon={PlusSignIcon}
className="size-5 text-muted-foreground group-hover:text-primary"
@ -234,7 +236,7 @@ export function BlockSheet({
type="button"
variant="ghost"
size="icon"
className={FLOATING_ICON_BUTTON_CLASS}
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={onImport}
>
<HugeiconsIcon
@ -246,7 +248,7 @@ export function BlockSheet({
type="button"
variant="ghost"
size="icon"
className={FLOATING_ICON_BUTTON_CLASS}
className={CANVAS_FLOATING_ICON_BUTTON_CLASS}
onClick={onCopy}
>
<HugeiconsIcon

View file

@ -0,0 +1,2 @@
export const CANVAS_FLOATING_ICON_BUTTON_CLASS =
"corner-squircle group h-11 w-11 rounded-xl border border-border/60 bg-transparent p-0 text-muted-foreground hover:bg-transparent hover:text-primary hover:border-primary/60";

View file

@ -10,7 +10,18 @@ export default defineConfig({
include: ["@dagrejs/dagre", "@dagrejs/graphlib"],
},
server: {
allowedHosts: ["playground.wasimhub.dev"],
host: "0.0.0.0",
allowedHosts: true,
proxy: {
"/api": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
},
"/preview-api": {
target: "http://127.0.0.1:8004",
changeOrigin: true,
},
},
},
resolve: {
alias: {