feat: add the waters
This commit is contained in:
@@ -25,8 +25,8 @@ function PlatformLayoutInner() {
|
|||||||
return (
|
return (
|
||||||
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
<SidebarProvider open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||||
<AppSidebar />
|
<AppSidebar />
|
||||||
<div className="relative flex min-h-0 w-full flex-1 flex-col bg-background">
|
<div className="relative flex h-dvh min-h-0 w-full flex-1 flex-col overflow-hidden bg-background">
|
||||||
<div className="flex min-h-0 flex-1 flex-col">
|
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import {
|
|||||||
} from './projectGraphStorage'
|
} from './projectGraphStorage'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { NewProjectDialog } from './NewProjectDialog'
|
import { NewProjectDialog } from './NewProjectDialog'
|
||||||
|
import { ProjectsPageBackground } from './ProjectsPageBackground'
|
||||||
import type { Project } from './types'
|
import type { Project } from './types'
|
||||||
|
|
||||||
const PAGE_SIZE_OPTIONS = [10, 25, 50] as const
|
const PAGE_SIZE_OPTIONS = [10, 25, 50] as const
|
||||||
@@ -315,7 +316,9 @@ export function ProjectsPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 flex-col gap-4 p-4">
|
<div className="relative flex flex-1 flex-col min-h-0">
|
||||||
|
<ProjectsPageBackground className="absolute inset-0 pointer-events-none" />
|
||||||
|
<div className="relative flex min-h-0 flex-1 flex-col gap-4 p-4 overflow-auto">
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<div className="flex flex-wrap items-center gap-3">
|
<div className="flex flex-wrap items-center gap-3">
|
||||||
<h1 className="p-3 scroll-m-20 text-4xl font-extrabold tracking-tight text-balance">
|
<h1 className="p-3 scroll-m-20 text-4xl font-extrabold tracking-tight text-balance">
|
||||||
@@ -797,6 +800,7 @@ export function ProjectsPage() {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
99
frontend/src/app/platform/ProjectsPageBackground.tsx
Normal file
99
frontend/src/app/platform/ProjectsPageBackground.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Subtle animated dot grid background for ProjectsPage.
|
||||||
|
* Matches canvas grid (20px gap), with gentle wave movement.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const GAP = 20
|
||||||
|
const SPEED = 0.010
|
||||||
|
const AMPLITUDE = 4
|
||||||
|
const DOT_SIZE = 1.2
|
||||||
|
const PULSE_AMOUNT = 0.25
|
||||||
|
|
||||||
|
function getDotColor(): string {
|
||||||
|
if (typeof document === 'undefined') return 'rgba(128,128,128,0.25)'
|
||||||
|
const isDark = document.documentElement.classList.contains('dark')
|
||||||
|
return isDark ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.12)'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectsPageBackground({ className }: { className?: string }) {
|
||||||
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||||
|
const timeRef = useRef(0)
|
||||||
|
const rafRef = useRef<number>(0)
|
||||||
|
|
||||||
|
const draw = useCallback(() => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const { width, height } = canvas
|
||||||
|
const cols = Math.floor(width / GAP) + 2
|
||||||
|
const rows = Math.floor(height / GAP) + 2
|
||||||
|
const t = timeRef.current
|
||||||
|
|
||||||
|
ctx.fillStyle = 'transparent'
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const dotColor = getDotColor()
|
||||||
|
|
||||||
|
for (let row = 0; row < rows; row++) {
|
||||||
|
for (let col = 0; col < cols; col++) {
|
||||||
|
const baseX = col * GAP
|
||||||
|
const baseY = row * GAP
|
||||||
|
|
||||||
|
const w1 = Math.sin(t * SPEED + baseX * 0.008 + baseY * 0.006) * AMPLITUDE
|
||||||
|
const w2 = Math.sin(t * SPEED * 0.7 - baseX * 0.012 + baseY * 0.01) * AMPLITUDE * 0.5
|
||||||
|
const w3 = Math.sin(t * SPEED * 1.2 + baseY * 0.015) * AMPLITUDE * 0.3
|
||||||
|
|
||||||
|
const x = baseX + w1 + w2 + w3
|
||||||
|
const y = baseY + w1 * 0.6 + w3 * 0.4
|
||||||
|
|
||||||
|
const size = DOT_SIZE + Math.sin(t * SPEED * 2 + baseX * 0.02) * PULSE_AMOUNT
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(x, y, size, 0, Math.PI * 2)
|
||||||
|
ctx.fillStyle = dotColor
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
timeRef.current += 1
|
||||||
|
rafRef.current = requestAnimationFrame(draw)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const canvas = canvasRef.current
|
||||||
|
if (!canvas) return
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
|
const dpr = Math.min(window.devicePixelRatio ?? 1, 2)
|
||||||
|
const rect = canvas.getBoundingClientRect()
|
||||||
|
canvas.width = Math.round(rect.width * dpr)
|
||||||
|
canvas.height = Math.round(rect.height * dpr)
|
||||||
|
canvas.style.width = `${rect.width}px`
|
||||||
|
canvas.style.height = `${rect.height}px`
|
||||||
|
const ctx = canvas.getContext('2d')
|
||||||
|
if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
resize()
|
||||||
|
window.addEventListener('resize', resize)
|
||||||
|
rafRef.current = requestAnimationFrame(draw)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', resize)
|
||||||
|
if (rafRef.current) cancelAnimationFrame(rafRef.current)
|
||||||
|
}
|
||||||
|
}, [draw])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
ref={canvasRef}
|
||||||
|
className={className}
|
||||||
|
aria-hidden
|
||||||
|
style={{ display: 'block', width: '100%', height: '100%' }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user