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:
parent
e39d03c21e
commit
0ed6c141b0
6 changed files with 107 additions and 10 deletions
1
studio/frontend/.gitignore
vendored
1
studio/frontend/.gitignore
vendored
|
|
@ -26,3 +26,4 @@ test/
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/src/features/canvas-lab/AGENTS.md
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue