fix: add brainstorm tables migration + calm ghost cursor
Some checks failed
CI / Lint, Test & Build (push) Failing after 7m59s
CI / Deploy production (on server) (push) Has been cancelled

- Create migration for BrainstormSession, BrainstormIdea, BrainstormNoteRef,
  BrainstormParticipant, BrainstormActivity, BrainstormShare, BrainstormSnapshot
- Ghost cursor: only moves toward a target element, no random wandering,
  120ms interval instead of 60fps, hidden when no target
- Remove animate-ping that caused visual noise
This commit is contained in:
Antigravity
2026-05-19 19:10:48 +00:00
parent 6a8d0eb0a5
commit 66c6f7ee8f
2 changed files with 198 additions and 57 deletions

View File

@@ -1,7 +1,6 @@
'use client'
import React, { useEffect, useRef } from 'react'
import { motion, AnimatePresence } from 'motion/react'
interface GhostCursorProps {
isActive: boolean
@@ -10,12 +9,9 @@ interface GhostCursorProps {
}
export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorProps) {
const positionRef = useRef({ x: 0, y: 0 })
const visibleRef = useRef(false)
const elRef = useRef<HTMLDivElement>(null)
const rafRef = useRef<number | null>(null)
const targetRef = useRef({ x: 0, y: 0 })
const initializedRef = useRef(false)
const posRef = useRef<{ x: number; y: number; visible: boolean }>({ x: 0, y: 0, visible: false })
const targetIdRef = useRef(targetId)
targetIdRef.current = targetId
const isActiveRef = useRef(isActive)
@@ -23,52 +19,34 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro
useEffect(() => {
if (!isActive) {
visibleRef.current = false
initializedRef.current = false
posRef.current.visible = false
if (elRef.current) elRef.current.style.opacity = '0'
if (rafRef.current) cancelAnimationFrame(rafRef.current)
return
}
let angle = Math.random() * Math.PI * 2
const init = () => {
const container = containerRef.current
if (!container) { rafRef.current = requestAnimationFrame(init); return }
const rect = container.getBoundingClientRect()
if (rect.width === 0 || rect.height === 0) { rafRef.current = requestAnimationFrame(init); return }
const cx = rect.width / 2
const cy = rect.height / 2
targetRef.current = { x: cx + (Math.random() - 0.5) * 200, y: cy + (Math.random() - 0.5) * 200 }
positionRef.current = { ...targetRef.current }
initializedRef.current = true
visibleRef.current = true
if (elRef.current) {
elRef.current.style.opacity = '1'
elRef.current.style.transform = `translate(${positionRef.current.x}px, ${positionRef.current.y}px)`
}
}
rafRef.current = requestAnimationFrame(init)
let lastMove = 0
const MOVE_INTERVAL = 120
const tick = () => {
if (!isActiveRef.current || !initializedRef.current) {
if (!isActiveRef.current) return
const now = performance.now()
if (now - lastMove < MOVE_INTERVAL) {
rafRef.current = requestAnimationFrame(tick)
return
}
lastMove = now
const container = containerRef.current
if (!container) { rafRef.current = requestAnimationFrame(tick); return }
const containerRect = container.getBoundingClientRect()
if (containerRect.width === 0 || containerRect.height === 0) { rafRef.current = requestAnimationFrame(tick); return }
const cx = containerRect.width / 2
const cy = containerRect.height / 2
let tx = targetRef.current.x
let ty = targetRef.current.y
const currentTargetId = targetIdRef.current
let tx: number | null = null
let ty: number | null = null
if (currentTargetId) {
const nodeElement = container.querySelector(`[data-id="${currentTargetId}"]`) as HTMLElement | null
if (nodeElement) {
@@ -78,29 +56,34 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro
}
}
const prev = positionRef.current
const dx = tx - prev.x
const dy = ty - prev.y
const dist = Math.sqrt(dx * dx + dy * dy)
if (!currentTargetId && dist < 20) {
angle = Math.random() * Math.PI * 2
const radius = 150 + Math.random() * 200
tx = cx + Math.cos(angle) * radius
ty = cy + Math.sin(angle) * radius
targetRef.current = { x: tx, y: ty }
if (tx === null || ty === null) {
if (posRef.current.visible) {
posRef.current.visible = false
if (elRef.current) elRef.current.style.opacity = '0'
}
rafRef.current = requestAnimationFrame(tick)
return
}
const speed = currentTargetId ? 0.12 : 0.04
let newX = prev.x + (tx - prev.x) * speed
let newY = prev.y + (ty - prev.y) * speed
if (isNaN(newX)) newX = cx
if (isNaN(newY)) newY = cy
const pos = posRef.current
const speed = 0.15
const newX = pos.x + (tx - pos.x) * speed
const newY = pos.y + (ty - pos.y) * speed
positionRef.current = { x: newX, y: newY }
if (elRef.current) {
elRef.current.style.transform = `translate(${newX}px, ${newY}px)`
if (!pos.visible) {
pos.x = tx
pos.y = ty
pos.visible = true
if (elRef.current) {
elRef.current.style.opacity = '1'
elRef.current.style.transform = `translate(${tx}px, ${ty}px)`
}
} else {
pos.x = isNaN(newX) ? tx : newX
pos.y = isNaN(newY) ? ty : newY
if (elRef.current) {
elRef.current.style.transform = `translate(${pos.x}px, ${pos.y}px)`
}
}
rafRef.current = requestAnimationFrame(tick)
@@ -121,7 +104,7 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro
left: 0,
top: 0,
opacity: 0,
transition: 'opacity 0.3s ease',
transition: 'opacity 0.4s ease, transform 0.15s ease-out',
}}
>
<div className="relative">
@@ -129,11 +112,10 @@ export function GhostCursor({ isActive, containerRef, targetId }: GhostCursorPro
<path d="M0 0L16 6L8 8L6 16L0 0Z" fill="#a78bfa" />
</svg>
<div className="absolute -top-1 -right-1 w-3 h-3">
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-violet-400 opacity-50" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-violet-500" />
</div>
<div className="mt-3 ml-3 px-2 py-0.5 rounded-full text-[10px] font-bold text-white whitespace-nowrap shadow-lg bg-gradient-to-r from-violet-500 to-purple-600">
AI
AI
</div>
</div>
</div>