diff --git a/app/globals.css b/app/globals.css index 574952a69..34c994559 100644 --- a/app/globals.css +++ b/app/globals.css @@ -69,10 +69,12 @@ @layer base { * { @apply border-border; + overscroll-behavior-x: none; } body { @apply bg-background text-foreground; + overscroll-behavior-x: none; } } @@ -90,4 +92,4 @@ input[type='search']::-moz-search-cancel-button { /* Microsoft Edge */ input[type='search']::-ms-clear { display: none; -} \ No newline at end of file +} diff --git a/app/w/[id]/workflow.tsx b/app/w/[id]/workflow.tsx index e3ffbda48..44467d324 100644 --- a/app/w/[id]/workflow.tsx +++ b/app/w/[id]/workflow.tsx @@ -1,16 +1,217 @@ +'use client' + +import { useState, useCallback, useEffect } from 'react' +import { BlockProps } from '../components/block' + +const ZOOM_SPEED = 0.005 +const MIN_ZOOM = 0.5 +const MAX_ZOOM = 2 +const CANVAS_SIZE = 5000 // 5000px x 5000px virtual canvas + export default function Workflow() { + const [blocks, setBlocks] = useState< + (BlockProps & { id: string; position: { x: number; y: number } })[] + >([]) + const [zoom, setZoom] = useState(1) + const [pan, setPan] = useState({ x: 0, y: 0 }) + const [isPanning, setIsPanning] = useState(false) + const [startPanPos, setStartPanPos] = useState({ x: 0, y: 0 }) + + // Initialize pan position after mount + useEffect(() => { + const viewportWidth = window.innerWidth - 344 // Account for sidebar + const viewportHeight = window.innerHeight - 56 // Account for header + setPan({ + x: (viewportWidth - CANVAS_SIZE) / 2, + y: (viewportHeight - CANVAS_SIZE) / 2, + }) + }, []) + + const constrainPan = useCallback( + (newPan: { x: number; y: number }, currentZoom: number) => { + // Calculate the visible area dimensions + const viewportWidth = window.innerWidth + const viewportHeight = window.innerHeight - 56 // Adjust for header height + + // Calculate the scaled canvas size + const scaledCanvasWidth = CANVAS_SIZE * currentZoom + const scaledCanvasHeight = CANVAS_SIZE * currentZoom + + // Calculate the maximum allowed pan values + const maxX = 0 + const minX = viewportWidth - scaledCanvasWidth + const maxY = 0 + const minY = viewportHeight - scaledCanvasHeight + + return { + x: Math.min(maxX, Math.max(minX, newPan.x)), + y: Math.min(maxY, Math.max(minY, newPan.y)), + } + }, + [] + ) + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault() + e.dataTransfer.dropEffect = 'copy' + } + + const handleDrop = (e: React.DragEvent) => { + e.preventDefault() + + try { + const blockData = JSON.parse( + e.dataTransfer.getData('application/json') + ) as BlockProps + + // Get the canvas element's bounding rectangle + const rect = e.currentTarget.getBoundingClientRect() + + // Calculate the drop position in canvas coordinates + // 1. Get the mouse position relative to the canvas element + // 2. Remove the pan offset (scaled by zoom) + // 3. Scale by zoom to get true canvas coordinates + const mouseX = e.clientX - rect.left + const mouseY = e.clientY - rect.top + + const x = mouseX / zoom + const y = mouseY / zoom + + setBlocks((prev) => [ + ...prev, + { + ...blockData, + id: crypto.randomUUID(), + position: { x, y }, + }, + ]) + } catch (err) { + console.error('Error dropping block:', err) + } + } + + const handleWheel = useCallback( + (e: React.WheelEvent) => { + // Prevent browser zooming + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + const delta = -e.deltaY * ZOOM_SPEED + setZoom((prevZoom) => { + // If we're at max/min zoom and trying to zoom further, return current zoom + if ( + (prevZoom >= MAX_ZOOM && delta > 0) || + (prevZoom <= MIN_ZOOM && delta < 0) + ) { + return prevZoom + } + const newZoom = Math.min( + MAX_ZOOM, + Math.max(MIN_ZOOM, prevZoom + delta) + ) + // Adjust pan when zooming to keep the point under cursor fixed + setPan((prevPan) => constrainPan(prevPan, newZoom)) + return newZoom + }) + } else { + // Regular scrolling for pan + setPan((prevPan) => + constrainPan( + { + x: prevPan.x - e.deltaX, + y: prevPan.y - e.deltaY, + }, + zoom + ) + ) + } + }, + [constrainPan, zoom] + ) + + const handleMouseDown = useCallback( + (e: React.MouseEvent) => { + if (e.button === 1 || e.button === 0) { + // Middle mouse or left click + setIsPanning(true) + setStartPanPos({ x: e.clientX - pan.x, y: e.clientY - pan.y }) + } + }, + [pan] + ) + + const handleMouseMove = useCallback( + (e: React.MouseEvent) => { + if (isPanning) { + setPan((prevPan) => + constrainPan( + { + x: e.clientX - startPanPos.x, + y: e.clientY - startPanPos.y, + }, + zoom + ) + ) + } + }, + [isPanning, startPanPos, zoom, constrainPan] + ) + + const handleMouseUp = useCallback(() => { + setIsPanning(false) + }, []) + + // Add this useEffect to prevent browser zoom + useEffect(() => { + const preventDefaultZoom = (e: WheelEvent) => { + if (e.ctrlKey || e.metaKey) { + e.preventDefault() + } + } + + document.addEventListener('wheel', preventDefaultZoom, { passive: false }) + return () => document.removeEventListener('wheel', preventDefaultZoom) + }, []) + return ( - //
- //
- // {/* Canvas content will go here */} - //
- //
- <> +
+
+ {blocks.map((block) => ( +
+ ))} +
+
) } diff --git a/app/w/components/block.tsx b/app/w/components/block.tsx index 55f8aebcd..dc4198102 100644 --- a/app/w/components/block.tsx +++ b/app/w/components/block.tsx @@ -15,8 +15,26 @@ export function Block({ type, bgColor, }: BlockProps) { + const handleDragStart = (e: React.DragEvent) => { + // Pass block data as JSON string + e.dataTransfer.setData( + 'application/json', + JSON.stringify({ + type, + title, + description, + imagePath, + bgColor, + }) + ) + } + return ( -
+
onTabChange('basic')} className={`text-sm font-medium transition-colors hover:text-black ${ - activeTab === 'basic' ? 'text-black' : 'text-gray-500' + activeTab === 'basic' ? 'text-black' : 'text-muted-foreground' }`} > Basic @@ -20,7 +20,7 @@ export function ToolbarTabs({ activeTab, onTabChange }: ToolbarTabsProps) {
-
+
+ {/* Left Section - Workflow Info */} +
+

Workflow 1

+

Saved 2 minutes ago

+
+ + {/* Middle Section - Reserved for future use */} +
+ + {/* Right Section - Actions */} +
+ + + + + + View History + Compare Versions + + + + + + + + + No new notifications + + + + +
+
+ ) +} diff --git a/app/w/layout.tsx b/app/w/layout.tsx index 46d8c245a..1fc42fe79 100644 --- a/app/w/layout.tsx +++ b/app/w/layout.tsx @@ -24,19 +24,9 @@ import { SheetTrigger, SheetTitle, } from '@/components/ui/sheet' -import { - Breadcrumb, - BreadcrumbItem, - BreadcrumbLink, - BreadcrumbList, - BreadcrumbPage, - BreadcrumbSeparator, -} from '@/components/ui/breadcrumb' -import { SearchInput } from './components/search' -import { User } from './components/user' import Image from 'next/image' -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { DesktopToolbar } from './components/desktop-toolbar' +import { WorkflowControlBar } from './components/workflow-control-bar' export default function WorkspaceLayout({ children, @@ -49,12 +39,15 @@ export default function WorkspaceLayout({
+
+ +
-
-
+
+
-
+
{children}