Skip to content

Commit a902f95

Browse files
authored
Merge pull request #3 from Radian-os/dev
Merge dev into main
2 parents fd7dcc2 + 55452f0 commit a902f95

15 files changed

Lines changed: 572 additions & 444 deletions

File tree

apps/website/next.config.js

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,29 @@ const nextConfig = {
1010
async headers() {
1111
return [
1212
{
13-
source: "/:path*",
13+
source: "/_next/static/:path*", // Static assets (JS, CSS, images)
1414
headers: [
1515
{
16-
key: "Referrer-Policy",
17-
value: "strict-origin-when-cross-origin",
16+
key: "Cache-Control",
17+
value: "public, max-age=2592000, immutable",
1818
},
19+
],
20+
},
21+
{
22+
source: "/:path*", // HTML pages and other
23+
headers: [
24+
// {
25+
// key: "Referrer-Policy",
26+
// value: "strict-origin-when-cross-origin",
27+
// },
1928
{
2029
key: "Strict-Transport-Security",
2130
value: "max-age=63072000; includeSubDomains; preload",
2231
},
23-
{
24-
key: "X-Content-Type-Options",
25-
value: "nosniff",
26-
},
32+
// {
33+
// key: "X-Content-Type-Options",
34+
// value: "nosniff",
35+
// },
2736
{
2837
key: "X-Frame-Options",
2938
value: "DENY",
@@ -34,7 +43,7 @@ const nextConfig = {
3443
},
3544
{
3645
key: "Cache-Control",
37-
value: "public, max-age=2592000, s-maxage=5184000, stale-while-revalidate=59",
46+
value: "public, max-age=60, stale-while-revalidate=30",
3847
},
3948
],
4049
},
@@ -74,6 +83,11 @@ const nextConfig = {
7483
destination: "/docs/components/accordion",
7584
permanent: true,
7685
},
86+
{
87+
source: "/figma",
88+
destination: "/docs/getting-started/figma",
89+
permanent: true,
90+
},
7791
]
7892
},
7993
skipTrailingSlashRedirect: true,

apps/website/src/app/(app)/blog/page.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ export const metadata = {
1717
keywords: ["RadianOS blog", "Radian UI Blogs", "Radian updates", "tech insights", "product updates", "Radian tips", "industry news"],
1818
author: "RadianOS Team",
1919
robots: "index, follow",
20-
viewport: "width=device-width, initial-scale=1",
2120
openGraph: {
2221
title: "RadianOS Blog | Latest Updates & Insights",
2322
description: "Read the latest updates and insights from the RadianOS team. Stay informed about product releases, industry trends, and expert tips.",

apps/website/src/app/blocks/signin/page.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,18 @@ interface SigninProps {
1616
export default function Page({ fullScreen = true }: SigninProps) {
1717
return (
1818
<div className={cn("bg-bg flex", { "h-screen w-screen": fullScreen, "h-full w-full": !fullScreen })}>
19-
<div className="flex w-full">
20-
<div className="relative hidden flex-1 md:block">
21-
<Image fill fetchPriority="high" priority={true} className="object-cover" src="/media/background-2-homepage.jpg" alt="Background Image" />
19+
<div className="flex h-full w-full">
20+
<div className="relative hidden h-full flex-1 md:block">
21+
<Image
22+
fill
23+
fetchPriority="high"
24+
priority={true}
25+
className="object-cover"
26+
src="/media/background-2-homepage.jpg"
27+
alt="Background Image"
28+
sizes="(max-width: 768px) 100vw, 900px"
29+
quality={80}
30+
/>
2231
</div>
2332
<div className="bg-bg flex h-full w-full flex-1 shrink-0 items-center justify-center p-5">
2433
<div className="w-90 flex flex-col gap-8">

apps/website/src/components/effects/background.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default function Background({ children }: { children?: React.ReactNode })
2121
flickerChance={0.3}
2222
/>
2323

24-
<div className="relative z-50 w-full">{children}</div>
24+
<div className="relative z-30 w-full">{children}</div>
2525
</div>
2626
)
2727
}
Lines changed: 116 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"use client"
22

3-
import React, { useEffect, useMemo, useRef } from "react"
3+
import React, { useCallback, useEffect, useRef, useState } from "react"
44
import { cn } from "@/lib/utils"
5-
import { getFlickeringGridWorker } from "./flickering-singleton"
5+
import { subscribeRAF } from "./raf-scheduler"
66

77
export interface FlickeringGridProps {
88
squareSize?: number
@@ -14,147 +14,159 @@ export interface FlickeringGridProps {
1414
className?: string
1515
maxOpacity?: number
1616
shape?: "circle" | "square" | "mixed"
17-
fps?: number
17+
}
18+
19+
interface GridState {
20+
ctx: CanvasRenderingContext2D
21+
cols: number
22+
rows: number
23+
squares: Float32Array
24+
shapes: Uint8Array
25+
step: number
1826
}
1927

2028
export const FlickeringGrid: React.FC<FlickeringGridProps> = ({
2129
squareSize = 4,
2230
gridGap = 6,
2331
flickerChance = 0.3,
24-
color = "rgb(0, 0, 0)",
32+
color = "rgb(0,0,0)",
2533
width,
2634
height,
2735
className,
2836
maxOpacity = 0.3,
2937
shape = "square",
30-
fps = 20,
3138
}) => {
3239
const canvasRef = useRef<HTMLCanvasElement>(null)
3340
const containerRef = useRef<HTMLDivElement>(null)
34-
const idRef = useRef<string>(typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : Math.random().toString(36).slice(2))
35-
36-
const rgbaPrefix = useMemo(() => {
37-
const toRGBA = (c: string) => {
38-
if (typeof window === "undefined") return `rgba(0, 0, 0,`
39-
const canvas = document.createElement("canvas")
40-
canvas.width = canvas.height = 1
41-
const ctx = canvas.getContext("2d", { alpha: true })
42-
if (!ctx) return "rgba(255, 0, 0,"
43-
ctx.fillStyle = c
44-
ctx.fillRect(0, 0, 1, 1)
45-
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
46-
return `rgba(${r}, ${g}, ${b},`
47-
}
48-
return toRGBA(color)
49-
}, [color])
41+
const gridRef = useRef<GridState | null>(null)
42+
const lastTimeRef = useRef(0)
5043

44+
const [isInView, setIsInView] = useState(false)
45+
const [rgbaBase, setRgbaBase] = useState("rgba(0,0,0,")
46+
47+
/* ---------- SSR-safe color parsing ---------- */
5148
useEffect(() => {
49+
const canvas = document.createElement("canvas")
50+
canvas.width = canvas.height = 1
51+
const ctx = canvas.getContext("2d")
52+
if (!ctx) return
53+
54+
ctx.fillStyle = color
55+
ctx.fillRect(0, 0, 1, 1)
56+
const [r, g, b] = ctx.getImageData(0, 0, 1, 1).data
57+
setRgbaBase(`rgba(${r},${g},${b},`)
58+
}, [color])
59+
60+
/* ---------- Canvas + grid setup ---------- */
61+
const setupGrid = useCallback(() => {
5262
const canvas = canvasRef.current
5363
const container = containerRef.current
5464
if (!canvas || !container) return
5565

56-
const worker = getFlickeringGridWorker()
66+
const w = width ?? container.clientWidth
67+
const h = height ?? container.clientHeight
68+
const dpr = window.devicePixelRatio || 1
5769

58-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
59-
const canOffscreen = typeof (canvas as any).transferControlToOffscreen === "function"
70+
canvas.width = w * dpr
71+
canvas.height = h * dpr
72+
canvas.style.width = `${w}px`
73+
canvas.style.height = `${h}px`
6074

61-
// Fallback: if OffscreenCanvas isn't available, do nothing here.
62-
// (You can keep your original main-thread implementation as fallback if needed.)
63-
if (!worker || !canOffscreen) {
64-
// Optional: you can paste your original implementation as a fallback.
65-
return
66-
}
75+
const ctx = canvas.getContext("2d")
76+
if (!ctx) return
6777

68-
const id = idRef.current
69-
const dpr = window.devicePixelRatio || 1
78+
ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
7079

71-
const getSize = () => ({
72-
w: width ?? container.clientWidth,
73-
h: height ?? container.clientHeight,
74-
})
80+
const step = squareSize + gridGap
81+
const cols = Math.floor(w / step)
82+
const rows = Math.floor(h / step)
7583

76-
const { w, h } = getSize()
84+
const squares = new Float32Array(cols * rows)
85+
const shapes = new Uint8Array(cols * rows)
7786

78-
// Style size (CSS pixels)
79-
canvas.style.width = `${w}px`
80-
canvas.style.height = `${h}px`
87+
for (let i = 0; i < squares.length; i++) {
88+
squares[i] = Math.random() * maxOpacity
89+
shapes[i] = shape === "mixed" ? (Math.random() < 0.5 ? 1 : 0) : shape === "circle" ? 1 : 0
90+
}
8191

82-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
83-
const offscreen = (canvas as any).transferControlToOffscreen() as OffscreenCanvas
84-
85-
worker.postMessage(
86-
{
87-
type: "init",
88-
id,
89-
canvas: offscreen,
90-
width: w,
91-
height: h,
92-
dpr,
93-
squareSize,
94-
gridGap,
95-
flickerChance,
96-
maxOpacity,
97-
rgbaPrefix,
98-
shape,
99-
fps,
100-
},
101-
[offscreen]
102-
)
92+
gridRef.current = { ctx, cols, rows, squares, shapes, step }
93+
}, [squareSize, gridGap, maxOpacity, shape, width, height])
94+
95+
/* ---------- Animation ---------- */
96+
const animate = useCallback(
97+
(time: number) => {
98+
if (!isInView || !gridRef.current) return
99+
100+
const delta = (time - lastTimeRef.current) / 1000
101+
if (delta < 0.05) return
102+
lastTimeRef.current = time
103+
104+
const { ctx, cols, rows, squares, shapes, step } = gridRef.current
105+
106+
const updates = Math.floor(squares.length * flickerChance * delta)
107+
for (let i = 0; i < updates; i++) {
108+
const idx = (Math.random() * squares.length) | 0
109+
squares[idx] = Math.random() * maxOpacity
110+
}
111+
112+
ctx.clearRect(0, 0, cols * step, rows * step)
113+
114+
for (let i = 0; i < cols; i++) {
115+
for (let j = 0; j < rows; j++) {
116+
const idx = i * rows + j
117+
const o = squares[idx]
118+
if (o < 0.01) continue
119+
120+
ctx.fillStyle = `${rgbaBase}${o})`
121+
const x = i * step
122+
const y = j * step
123+
124+
if (shapes[idx]) {
125+
ctx.beginPath()
126+
ctx.arc(x + squareSize / 2, y + squareSize / 2, squareSize / 2, 0, Math.PI * 2)
127+
ctx.fill()
128+
} else {
129+
ctx.fillRect(x, y, squareSize, squareSize)
130+
}
131+
}
132+
}
133+
},
134+
[isInView, flickerChance, maxOpacity, rgbaBase, squareSize]
135+
)
136+
137+
useEffect(() => {
138+
setupGrid()
103139

104140
const resizeObserver = new ResizeObserver(() => {
105-
const { w: newW, h: newH } = getSize()
106-
canvas.style.width = `${newW}px`
107-
canvas.style.height = `${newH}px`
108-
109-
worker.postMessage({
110-
type: "resize",
111-
id,
112-
width: newW,
113-
height: newH,
114-
dpr: window.devicePixelRatio || 1,
115-
})
141+
setupGrid()
116142
})
117-
resizeObserver.observe(container)
118-
119-
const intersectionObserver = new IntersectionObserver(
120-
([entry]) => {
121-
worker.postMessage({
122-
type: "visibility",
123-
id,
124-
inView: entry.isIntersecting,
125-
})
126-
},
127-
{ threshold: 0 }
128-
)
129-
intersectionObserver.observe(canvas)
143+
144+
if (containerRef.current) {
145+
resizeObserver.observe(containerRef.current)
146+
}
147+
148+
const intersectionObserver = new IntersectionObserver(([entry]) => {
149+
setIsInView(entry.isIntersecting)
150+
})
151+
152+
if (canvasRef.current) {
153+
intersectionObserver.observe(canvasRef.current)
154+
}
130155

131156
return () => {
132157
resizeObserver.disconnect()
133158
intersectionObserver.disconnect()
134-
worker.postMessage({ type: "destroy", id })
135159
}
136-
}, [width, height]) // size is handled by observer; this just re-inits if fixed props change
160+
}, [setupGrid])
137161

138-
// Prop updates (color, flicker settings, etc.)
139162
useEffect(() => {
140-
const worker = getFlickeringGridWorker()
141-
if (!worker) return
142-
worker.postMessage({
143-
type: "update",
144-
id: idRef.current,
145-
squareSize,
146-
gridGap,
147-
flickerChance,
148-
maxOpacity,
149-
rgbaPrefix,
150-
shape,
151-
fps,
152-
})
153-
}, [squareSize, gridGap, flickerChance, maxOpacity, rgbaPrefix, shape, fps])
163+
if (!isInView) return
164+
return subscribeRAF(animate)
165+
}, [isInView, animate])
154166

155167
return (
156-
<div ref={containerRef} className={cn("h-full w-full bg-transparent", className)}>
157-
<canvas ref={canvasRef} className="pointer-events-none" style={{ backgroundColor: "transparent" }} />
168+
<div ref={containerRef} className={cn("h-full w-full", className)}>
169+
<canvas ref={canvasRef} className="pointer-events-none" />
158170
</div>
159171
)
160172
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
type RAFCallback = (time: number) => void
2+
3+
const subscribers = new Set<RAFCallback>()
4+
let rafId: number | null = null
5+
6+
function tick(time: number) {
7+
subscribers.forEach((cb) => cb(time))
8+
rafId = requestAnimationFrame(tick)
9+
}
10+
11+
export function subscribeRAF(cb: RAFCallback) {
12+
subscribers.add(cb)
13+
if (!rafId) rafId = requestAnimationFrame(tick)
14+
15+
return () => {
16+
subscribers.delete(cb)
17+
if (subscribers.size === 0 && rafId) {
18+
cancelAnimationFrame(rafId)
19+
rafId = null
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)