-
Notifications
You must be signed in to change notification settings - Fork 0
Development #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Development #14
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 ( | ||
| <div style={shapeStyles}> | ||
| <Handle | ||
| type="target" | ||
| position={Position.Top} | ||
| className="!bg-text-muted !border-border-default !h-3 !w-3" | ||
| /> | ||
| <span | ||
| className="px-3 py-1 text-sm font-medium text-center" | ||
| style={{ color: colorPair.text }} | ||
| > | ||
| {label || "Node"} | ||
| </span> | ||
| <Handle | ||
| type="source" | ||
| position={Position.Bottom} | ||
| className="!bg-text-muted !border-border-default !h-3 !w-3" | ||
|
Comment on lines
+51
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '\n== Next.js docs mentioning important modifier syntax ==\n'
fd -a 'tailwind' node_modules/next/dist/docs 2>/dev/null | head -20 || true
rg -n "important.*suffix|suffix syntax|Tailwind v4|important modifier" node_modules/next/dist/docs -S 2>/dev/null | head -80 || true
printf '\n== Tailwind config / package signals ==\n'
git ls-files | rg '(^|/)(tailwind\.config|postcss\.config|package\.json|components/editor/(canvas-node|canvas)\.tsx)$'
printf '\n--- package.json ---\n'
cat package.json | sed -n '1,220p'
printf '\n== Tailwind-related files ==\n'
for f in $(git ls-files | rg '(^|/)(tailwind\.config|postcss\.config|components/editor/(canvas-node|canvas)\.tsx)$'); do
echo "\n--- $f ---"
wc -l "$f"
cat -n "$f" | sed -n '1,220p'
doneRepository: tejasM17/ghost-dev Length of output: 12593 🌐 Web query:
💡 Result: In Tailwind CSS v4, the correct syntax for the important modifier is to place the exclamation mark (!) at the end of the utility class, such as bg-red-500! [1][2]. This is a breaking change from Tailwind CSS v3, where the modifier was placed at the beginning of the class name (e.g.,!bg-red-500) [3][4][2]. The new suffix-based syntax aligns with native CSS, where!important follows the declaration [3]. For projects requiring all utilities to be marked as!important globally, Tailwind CSS v4 supports an important flag during import in your CSS file [5][6]: Citations:
Switch these Tailwind important modifiers to the v4 suffix form.
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 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"; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}`; | ||
|
Comment on lines
+123
to
+130
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Make node IDs unique across collaborating clients. The counter is local to each browser, so two clients dropping the same shape within one millisecond can generate identical IDs and corrupt subsequent node operations. Retain the requested prefix but append 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * 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 ( | ||
| <ReactFlow | ||
| nodes={nodes} | ||
| edges={edges} | ||
| onNodesChange={onNodesChange} | ||
| onEdgesChange={onEdgesChange} | ||
| onConnect={onConnect} | ||
| onDelete={onDelete} | ||
| fitView | ||
| connectionMode={ConnectionMode.Loose} | ||
| proOptions={{ hideAttribution: true }} | ||
| className="bg-bg-base" | ||
| > | ||
| <Background | ||
| variant={BackgroundVariant.Dots} | ||
| gap={18} | ||
| size={1} | ||
| color="var(--border-subtle)" | ||
| /> | ||
| <MiniMap | ||
| pannable | ||
| zoomable | ||
| className="!bg-bg-surface !border !border-border-default !rounded-xl" | ||
| nodeColor="var(--text-muted)" | ||
| maskColor="rgba(8, 8, 9, 0.7)" | ||
| <ReactFlowProvider> | ||
| <FlowCanvas | ||
| nodes={nodes} | ||
| edges={edges} | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| onNodesChange={onNodesChange as any} | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| onEdgesChange={onEdgesChange as any} | ||
| onConnect={onConnect} | ||
| // eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
| onDelete={onDelete as any} | ||
| /> | ||
| </ReactFlow> | ||
| </ReactFlowProvider> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 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<CanvasNode>[]) => void; | ||
| onEdgesChange: (changes: EdgeChange<CanvasEdge>[]) => 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; | ||
|
|
||
|
Comment on lines
+207
to
+214
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Validate the parsed drag payload before persisting it. A successful 🤖 Prompt for AI Agents |
||
| // 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<CanvasNode> = { | ||
| type: "add", | ||
| item: newNode, | ||
| }; | ||
| onNodesChange([addChange]); | ||
| } catch (error) { | ||
| console.error("Failed to parse shape payload:", error); | ||
| } | ||
| }, | ||
| [screenToFlowPosition, onNodesChange], | ||
| ); | ||
|
|
||
| return ( | ||
| <div | ||
| className="absolute inset-0" | ||
| onDragOver={handleDragOver} | ||
| onDrop={handleDrop} | ||
| > | ||
| <ReactFlow | ||
| nodes={nodes} | ||
| edges={edges} | ||
| onNodesChange={onNodesChange} | ||
| onEdgesChange={onEdgesChange} | ||
| onConnect={onConnect} | ||
| onDelete={onDelete} | ||
| nodeTypes={nodeTypes} | ||
| fitView | ||
| connectionMode={ConnectionMode.Loose} | ||
| proOptions={{ hideAttribution: true }} | ||
| className="!bg-transparent" | ||
| > | ||
| <Background | ||
| variant={BackgroundVariant.Dots} | ||
| gap={20} | ||
| size={1} | ||
| color="var(--border-subtle)" | ||
| /> | ||
| <MiniMap | ||
| pannable | ||
| zoomable | ||
| className="!bg-bg-surface/80 !rounded-lg" | ||
| nodeColor="var(--text-muted)" | ||
| maskColor="rgba(8, 8, 9, 0.6)" | ||
| /> | ||
| </ReactFlow> | ||
| <ShapePanel /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * 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 ( | ||
| <div className="flex h-full items-center justify-center bg-bg-base"> | ||
| <div className="flex h-full items-center justify-center bg-transparent"> | ||
| <div className="flex flex-col items-center gap-3"> | ||
| <div className="h-2 w-32 overflow-hidden rounded-full bg-bg-elevated"> | ||
| <div className="h-1.5 w-28 overflow-hidden rounded-full bg-bg-surface/50"> | ||
| <div className="h-full w-1/2 animate-pulse rounded-full bg-accent-primary" /> | ||
| </div> | ||
| <p className="text-xs text-text-muted"> | ||
|
|
@@ -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 ( | ||
| <div className="flex h-full items-center justify-center bg-bg-base"> | ||
| <div className="flex h-full items-center justify-center bg-transparent"> | ||
| <div className="flex max-w-md flex-col items-center gap-4 text-center"> | ||
| <div className="rounded-2xl border border-state-error/30 bg-bg-surface p-6"> | ||
| <div className="rounded-2xl border border-state-error/30 bg-bg-surface/80 p-6 backdrop-blur-sm"> | ||
| <h3 className="text-sm font-semibold text-text-primary"> | ||
| Canvas unavailable | ||
| </h3> | ||
|
|
@@ -199,7 +336,7 @@ function CanvasError({ message, onRetry }: CanvasErrorProps) { | |
| <button | ||
| type="button" | ||
| onClick={onRetry} | ||
| className="rounded-xl bg-bg-elevated px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-subtle" | ||
| className="rounded-xl bg-bg-surface/80 px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-elevated backdrop-blur-sm" | ||
| > | ||
| Try again | ||
| </button> | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Include the border style in the shorthand.
border: "2px <color>"is invalid CSS, so nodes do not receive the required border.Proposed fix
📝 Committable suggestion
🤖 Prompt for AI Agents