From 7cbb9db8b210569de7f2aebfe60c6ceb3cb05ad8 Mon Sep 17 00:00:00 2001 From: tejasM17 Date: Thu, 16 Jul 2026 11:56:47 +0530 Subject: [PATCH 1/2] deleted unwanted files --- context/feature-specs/frontend-componants.md | 80 -------------------- 1 file changed, 80 deletions(-) delete mode 100644 context/feature-specs/frontend-componants.md diff --git a/context/feature-specs/frontend-componants.md b/context/feature-specs/frontend-componants.md deleted file mode 100644 index 05e080c..0000000 --- a/context/feature-specs/frontend-componants.md +++ /dev/null @@ -1,80 +0,0 @@ -# Component Reference - -Clerk offers a comprehensive suite of components designed to seamlessly integrate authentication and multi-tenancy into your application. With Clerk components, you can easily customize the appearance of authentication components and pages, manage the entire authentication flow to suit your specific needs, and even build robust SaaS applications. - -## Authentication components - -- [](https://clerk.com/docs/nextjs/reference/components/authentication/sign-in.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/sign-up.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/google-one-tap.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/oauth-consent.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/task-choose-organization.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/task-reset-password.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/task-setup-mfa.md) -- [](https://clerk.com/docs/nextjs/reference/components/authentication/waitlist.md) - -## User components - -- [](https://clerk.com/docs/nextjs/reference/components/user/user-avatar.md) -- [](https://clerk.com/docs/nextjs/reference/components/user/user-button.md) -- [](https://clerk.com/docs/nextjs/reference/components/user/user-profile.md) - -## Organization components - -- [](https://clerk.com/docs/nextjs/reference/components/organization/create-organization.md) -- [](https://clerk.com/docs/nextjs/reference/components/organization/organization-profile.md) -- [](https://clerk.com/docs/nextjs/reference/components/organization/organization-switcher.md) -- [](https://clerk.com/docs/nextjs/reference/components/organization/organization-list.md) - -## Billing components - -- [](https://clerk.com/docs/nextjs/reference/components/billing/pricing-table.md) -- [](https://clerk.com/docs/nextjs/reference/components/billing/checkout-button.md) -- [](https://clerk.com/docs/nextjs/reference/components/billing/plan-details-button.md) -- [](https://clerk.com/docs/nextjs/reference/components/billing/subscription-details-button.md) - -## Control components - -Control components manage authentication-related behaviors in your application. They handle tasks such as controlling content visibility based on user authentication status, managing loading states during authentication processes, and redirecting users to appropriate pages. Control components render at `` and `` states for assertions on the [Clerk object](https://clerk.com/docs/nextjs/reference/objects/clerk.md). A common example is the [](https://clerk.com/docs/nextjs/reference/components/control/show.md) component, which allows you to conditionally render content based on authentication and authorization state. - -- [](https://clerk.com/docs/nextjs/reference/components/control/authenticate-with-redirect-callback.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/clerk-degraded.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/clerk-failed.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/clerk-loaded.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/clerk-loading.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-create-organization.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-organization-profile.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-sign-in.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-sign-up.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-tasks.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/redirect-to-user-profile.md) -- [](https://clerk.com/docs/nextjs/reference/components/control/show.md) - -## Unstyled components - -- [](https://clerk.com/docs/nextjs/reference/components/unstyled/sign-in-button.md) -- [](https://clerk.com/docs/nextjs/reference/components/unstyled/sign-in-with-metamask.md) -- [](https://clerk.com/docs/nextjs/reference/components/unstyled/sign-up-button.md) -- [](https://clerk.com/docs/nextjs/reference/components/unstyled/sign-out-button.md) - -## Utilities components - -- [](https://clerk.com/docs/nextjs/reference/components/utilities/portal-provider.md) - -## Customization guides - -- [Customize components with the appearance prop](https://clerk.com/docs/nextjs/guides/customizing-clerk/appearance-prop/overview.md) -- [Localize components with the `localization` prop (experimental)](https://clerk.com/docs/guides/customizing-clerk/localization.md) -- [Add pages to the component](https://clerk.com/docs/nextjs/guides/customizing-clerk/adding-items/user-profile.md) -- [Add pages to the component](https://clerk.com/docs/nextjs/guides/customizing-clerk/adding-items/organization-profile.md) - -### Secured by Clerk branding - -> This feature requires a [paid plan](https://clerk.com/pricing){{ target: '_blank' }} for production use, but all features are free to use in development mode so that you can try out what works for you. See the [pricing](https://clerk.com/pricing){{ target: '_blank' }} page for more information. - -By default, Clerk displays a **Secured by Clerk** badge on Clerk components. You can remove this branding by following these steps: - -1. In the Clerk Dashboard, navigate to your application's [**Settings**](https://dashboard.clerk.com/~/settings). -2. Under **Branding**, toggle on the **Remove "Secured by Clerk" branding** option. - -- [Need help?](https://clerk.com/contact/support): Contact the support team to get answers to your questions. From 9373d83b87dee873a90943a72102da5fd33107d7 Mon Sep 17 00:00:00 2001 From: tejasM17 Date: Thu, 16 Jul 2026 12:44:40 +0530 Subject: [PATCH 2/2] implemented canvas with drag and drop functions --- components/editor/canvas-node.tsx | 83 ++++++++++ components/editor/canvas.tsx | 203 ++++++++++++++++++++---- components/editor/shape-panel.tsx | 117 ++++++++++++++ components/editor/workspace-shell.tsx | 65 ++++---- context/feature-specs/12-shape-panel.md | 43 +++++ context/progress-tracker.md | 8 +- 6 files changed, 452 insertions(+), 67 deletions(-) create mode 100644 components/editor/canvas-node.tsx create mode 100644 components/editor/shape-panel.tsx create mode 100644 context/feature-specs/12-shape-panel.md diff --git a/components/editor/canvas-node.tsx b/components/editor/canvas-node.tsx new file mode 100644 index 0000000..51d3b5e --- /dev/null +++ b/components/editor/canvas-node.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { memo, useMemo } from "react"; +import { Handle, Position, type NodeProps } from "@xyflow/react"; + +import { NODE_COLORS, type NodeColorName, type NodeShape } from "@/types/canvas"; + +/** + * Data attached to canvas nodes. + */ +interface CanvasNodeData { + label: string; + color: NodeColorName; + shape: NodeShape; +} + +/** + * Basic canvas node renderer. For this unit, renders every shape as a + * simple bordered rectangle with the label centered. Shape-specific + * visuals will be added later. + */ +function CanvasNode({ data, selected }: NodeProps) { + const { label, color, shape } = data as unknown as CanvasNodeData; + + // Get color from palette + const colorPair = useMemo(() => { + return NODE_COLORS.find((c) => c.name === color) ?? NODE_COLORS[0]; + }, [color]); + + // Basic shape styles - for now, all shapes render as bordered rectangles + // Shape-specific rendering will be added in future features + const shapeStyles = useMemo(() => { + const baseStyles = { + width: "100%", + height: "100%", + display: "flex", + alignItems: "center", + justifyContent: "center", + borderRadius: shape === "circle" ? "9999px" : shape === "pill" ? "9999px" : "8px", + backgroundColor: colorPair.fill, + border: `2px ${selected ? "var(--accent-primary)" : colorPair.fill}`, + boxShadow: selected + ? `0 0 0 2px var(--accent-primary), 0 4px 12px rgba(0,0,0,0.3)` + : "none", + }; + return baseStyles; + }, [colorPair.fill, selected, shape]); + + return ( +
+ + + {label || "Node"} + + +
+ ); +} + +/** + * Memoized custom node component for React Flow. + */ +export const CanvasNodeComponent = memo(CanvasNode, (prev, next) => { + return ( + prev.data.label === next.data.label && + prev.data.color === next.data.color && + prev.data.shape === next.data.shape && + prev.selected === next.selected + ); +}); + +CanvasNodeComponent.displayName = "CanvasNode"; \ No newline at end of file diff --git a/components/editor/canvas.tsx b/components/editor/canvas.tsx index 10c72ae..11d9c5d 100644 --- a/components/editor/canvas.tsx +++ b/components/editor/canvas.tsx @@ -2,8 +2,11 @@ import { Component, + useCallback, useEffect, + useMemo, useState, + type DragEvent, type ErrorInfo, type ReactNode, } from "react"; @@ -13,6 +16,12 @@ import { ConnectionMode, MiniMap, ReactFlow, + ReactFlowProvider, + useReactFlow, + type NodeChange, + type EdgeChange, + type OnConnect, + type OnDelete, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; @@ -27,7 +36,12 @@ import "@liveblocks/react-flow/styles.css"; import type { CanvasEdge, CanvasNode, + CanvasNodeData, + NodeShape, } from "@/types/canvas"; +import { NODE_COLORS, type NodeColorName } from "@/types/canvas"; +import { ShapePanel, type ShapePanelPayload } from "./shape-panel"; +import { CanvasNodeComponent } from "./canvas-node"; interface CanvasProps { roomId: string; @@ -103,9 +117,25 @@ class CanvasErrorBoundary extends Component< } } +/** + * Counter for generating unique node IDs. + */ +let nodeIdCounter = 0; + +/** + * Generate a unique node ID using shape name, timestamp, and counter. + */ +function generateNodeId(shape: NodeShape): string { + const timestamp = Date.now(); + return `${shape}-${timestamp}-${++nodeIdCounter}`; +} + /** * The actual React Flow surface, rendered after Liveblocks has connected * and the storage layer is ready (via Suspense). + * + * Note: This component is wrapped with ReactFlowProvider to allow using + * useReactFlow() hook for coordinate conversion. */ function CollaborativeFlow() { // `useLiveblocksFlow` returns the synced nodes/edges plus change @@ -119,38 +149,145 @@ function CollaborativeFlow() { }); return ( - - - + - + + ); +} + +/** + * Inner component that has access to ReactFlow context via ReactFlowProvider. + * This is where we can use useReactFlow() hook. + */ +function FlowCanvas({ + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + onDelete, +}: { + nodes: CanvasNode[]; + edges: CanvasEdge[]; + onNodesChange: (changes: NodeChange[]) => void; + onEdgesChange: (changes: EdgeChange[]) => void; + onConnect: OnConnect; + onDelete: (params: { nodes: CanvasNode[]; edges: CanvasEdge[] }) => void; +}) { + // Get the React Flow instance for coordinate conversion + const { screenToFlowPosition } = useReactFlow(); + + // Custom node types - memoized to keep references stable + const nodeTypes = useMemo( + () => ({ canvasNode: CanvasNodeComponent }), + [], + ); + + // Handle drag over on the canvas wrapper + const handleDragOver = useCallback((event: DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + }, []); + + // Handle drop on the canvas wrapper + const handleDrop = useCallback( + (event: DragEvent) => { + event.preventDefault(); + + // Read the shape payload from dataTransfer + const payloadData = event.dataTransfer.getData("application/json"); + if (!payloadData) return; + + try { + const payload: ShapePanelPayload = JSON.parse(payloadData); + const { shape, width, height } = payload; + + // Convert screen position to canvas coordinates + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + + // Create new node - use CanvasNode type from types/canvas + const newNode: CanvasNode = { + id: generateNodeId(shape), + type: "canvasNode", + position, + data: { + label: "", + color: NODE_COLORS[0].name as NodeColorName, + shape, + }, + measured: { + width, + height, + }, + }; + + // Add the node via onNodesChange with add change + const addChange: NodeChange = { + type: "add", + item: newNode, + }; + onNodesChange([addChange]); + } catch (error) { + console.error("Failed to parse shape payload:", error); + } + }, + [screenToFlowPosition, onNodesChange], + ); + + return ( +
+ + + + + +
); } /** - * Loading state shown inside the Liveblocks Suspense boundary. Matches - * the editor's dark surface palette so it doesn't flash a light fallback. + * Loading state shown inside the Liveblocks Suspense boundary. + * Uses transparent background to blend with the infinite canvas. */ function CanvasLoading() { const [dots, setDots] = useState(1); @@ -163,9 +300,9 @@ function CanvasLoading() { }, []); return ( -
+
-
+

@@ -184,13 +321,13 @@ interface CanvasErrorProps { /** * Fallback rendered when the Liveblocks connection cannot be established * (auth failure, room ID changed, full room, etc.). Keeps the user on the - * page with a clear next step. + * page with a clear next step. Uses transparent background for infinite canvas look. */ function CanvasError({ message, onRetry }: CanvasErrorProps) { return ( -

+
-
+

Canvas unavailable

@@ -199,7 +336,7 @@ function CanvasError({ message, onRetry }: CanvasErrorProps) { diff --git a/components/editor/shape-panel.tsx b/components/editor/shape-panel.tsx new file mode 100644 index 0000000..d3b0867 --- /dev/null +++ b/components/editor/shape-panel.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useCallback, useRef, useState } from "react"; +import { + Square, + Diamond, + Circle, + Pill, + Cylinder, + Hexagon, + type LucideIcon, +} from "lucide-react"; + +import type { NodeShape } from "@/types/canvas"; + +/** + * Shape definitions with their icons and default sizes. + * Default sizes follow the spec: rectangles wider than tall, circles square, + * diamonds slightly larger for labels. + */ +const SHAPES: Array<{ + name: NodeShape; + icon: LucideIcon; + width: number; + height: number; +}> = [ + { name: "rectangle", icon: Square, width: 180, height: 100 }, + { name: "diamond", icon: Diamond, width: 140, height: 140 }, + { name: "circle", icon: Circle, width: 120, height: 120 }, + { name: "pill", icon: Pill, width: 180, height: 80 }, + { name: "cylinder", icon: Cylinder, width: 100, height: 120 }, + { name: "hexagon", icon: Hexagon, width: 140, height: 120 }, +]; + +/** + * Shape panel payload transferred via drag-and-drop. + */ +export interface ShapePanelPayload { + shape: NodeShape; + width: number; + height: number; +} + +/** + * Floating pill-shaped toolbar at the bottom-center of the canvas. + * Contains draggable icon buttons for each supported shape. + */ +export function ShapePanel() { + return ( +
+
+ {SHAPES.map((shape) => ( + + ))} +
+
+ ); +} + +interface ShapeButtonProps { + name: NodeShape; + Icon: LucideIcon; + width: number; + height: number; +} + +function ShapeButton({ name, Icon, width, height }: ShapeButtonProps) { + const [isDragging, setIsDragging] = useState(false); + + const handleDragStart = useCallback( + (e: React.DragEvent) => { + const payload: ShapePanelPayload = { shape: name, width, height }; + e.dataTransfer.setData("application/json", JSON.stringify(payload)); + e.dataTransfer.effectAllowed = "copy"; + setIsDragging(true); + }, + [name, width, height], + ); + + const handleDragEnd = useCallback(() => { + setIsDragging(false); + }, []); + + return ( + + ); +} \ No newline at end of file diff --git a/components/editor/workspace-shell.tsx b/components/editor/workspace-shell.tsx index f7eafa9..19bab94 100644 --- a/components/editor/workspace-shell.tsx +++ b/components/editor/workspace-shell.tsx @@ -57,20 +57,21 @@ export function WorkspaceShell({ aiSidebarOpen={isAiSidebarOpen} onShareClick={() => {}} /> -
- setIsSidebarOpen(false)} - ownedProjects={ownedProjects} - sharedProjects={sharedProjects} - currentRoomId={currentRoomId} - /> + {/* Canvas area with access denied message */} +
- setIsAiSidebarOpen(false)} - /> -
+
+ setIsSidebarOpen(false)} + ownedProjects={ownedProjects} + sharedProjects={sharedProjects} + currentRoomId={currentRoomId} + /> + setIsAiSidebarOpen(false)} + />
); } @@ -85,20 +86,22 @@ export function WorkspaceShell({ aiSidebarOpen={isAiSidebarOpen} onShareClick={shareDialog.onOpenChange} /> -
- setIsSidebarOpen(false)} - ownedProjects={ownedProjects} - sharedProjects={sharedProjects} - currentRoomId={currentRoomId} - /> + {/* Canvas fills full viewport - sidebars float over it */} +
- setIsAiSidebarOpen(false)} - /> -
+
+ {/* Sidebars rendered outside canvas container so they float over it */} + setIsSidebarOpen(false)} + ownedProjects={ownedProjects} + sharedProjects={sharedProjects} + currentRoomId={currentRoomId} + /> + setIsAiSidebarOpen(false)} + /> {/* Share Dialog */}
@@ -386,7 +389,7 @@ function AiSidebar({ isOpen, onClose }: AiSidebarProps) { onClick={onClose} aria-label="Close AI chat" className={cn( - "fixed inset-0 z-30 bg-bg-base/60 backdrop-blur-sm transition-opacity duration-300 ease-in-out md:hidden", + "fixed inset-0 z-40 bg-bg-base/60 backdrop-blur-sm transition-opacity duration-300 ease-in-out md:hidden", isOpen ? "pointer-events-auto opacity-100" : "pointer-events-none opacity-0", @@ -395,7 +398,7 @@ function AiSidebar({ isOpen, onClose }: AiSidebarProps) {