CI / changes (push) Successful in 11s
CI / commitlint (push) Has been skipped
CI / openapi (push) Has been skipped
CI / web (push) Successful in 1m7s
CI / go (push) Has been skipped
CI / bird2 (push) Has been skipped
CI / release (push) Successful in 4m21s
Refactored the DashboardQuickLinks component to utilize the new DashboardQuickLinkCard for better organization and presentation. Enhanced the DashboardComponent to integrate quick links within a DashboardFramePanel, improving the overall user experience. Updated the metric-tile component import path for consistency. Removed unnecessary imports from the separator component to streamline the codebase.
88 lines
2.3 KiB
TypeScript
88 lines
2.3 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useRef } from 'react'
|
|
|
|
import { cn } from '@evobgp/ui/lib/utils'
|
|
|
|
function grain(i: number, j: number) {
|
|
const n = Math.sin(i * 127.1 + j * 311.7) * 43758.5453
|
|
return n - Math.floor(n)
|
|
}
|
|
|
|
/**
|
|
* Static dot field adapted from REUI card-17: theme-aware canvas dots,
|
|
* repainted on resize and theme change.
|
|
*/
|
|
export function CardDotField({ className }: { className?: string }) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
|
|
const GAP = 3
|
|
const DOT = 1.5
|
|
const BASE = 0.05
|
|
const PEAK = 0.32
|
|
|
|
const draw = () => {
|
|
const rect = canvas.getBoundingClientRect()
|
|
if (!rect.width || !rect.height) return
|
|
|
|
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
|
canvas.width = Math.round(rect.width * dpr)
|
|
canvas.height = Math.round(rect.height * dpr)
|
|
|
|
ctx.fillStyle = getComputedStyle(canvas).color || 'rgb(115,115,115)'
|
|
ctx.fillRect(0, 0, 1, 1)
|
|
const px = ctx.getImageData(0, 0, 1, 1).data
|
|
const color = `rgb(${px[0]}, ${px[1]}, ${px[2]})`
|
|
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
|
|
ctx.clearRect(0, 0, rect.width, rect.height)
|
|
ctx.fillStyle = color
|
|
|
|
const cols = Math.ceil(rect.width / GAP) + 1
|
|
const rows = Math.ceil(rect.height / GAP) + 1
|
|
for (let i = 0; i < cols; i++) {
|
|
const x = i * GAP
|
|
for (let j = 0; j < rows; j++) {
|
|
const q = grain(i, j)
|
|
const amp = 0.7 + 0.6 * grain(j * 2 + 1, i * 2 + 1)
|
|
let a = (BASE + (PEAK - BASE) * q * q) * amp
|
|
if (a > 1) a = 1
|
|
ctx.globalAlpha = a
|
|
ctx.fillRect(x, j * GAP, DOT, DOT)
|
|
}
|
|
}
|
|
ctx.globalAlpha = 1
|
|
}
|
|
|
|
draw()
|
|
|
|
const resizeObserver = new ResizeObserver(() => draw())
|
|
resizeObserver.observe(canvas)
|
|
|
|
const themeObserver = new MutationObserver(() => draw())
|
|
themeObserver.observe(document.documentElement, {
|
|
attributes: true,
|
|
attributeFilter: ['class', 'style'],
|
|
})
|
|
|
|
return () => {
|
|
resizeObserver.disconnect()
|
|
themeObserver.disconnect()
|
|
}
|
|
}, [])
|
|
|
|
return (
|
|
<canvas
|
|
ref={canvasRef}
|
|
aria-hidden="true"
|
|
className={cn('pointer-events-none absolute inset-0 h-full w-full', className)}
|
|
/>
|
|
)
|
|
}
|