From b7ee5b3d19807bb388335e2fda47fb97108fbc35 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 22 Jan 2026 22:03:36 -0800 Subject: [PATCH 1/2] feat(billing): gate billing page behind feature flag Hide billing settings from users who don't have the billing-enabled feature flag. Uses centralized FEATURE_FLAGS constant from shared. --- .../_authenticated/settings/billing/page.tsx | 13 ++++++++++++- .../settings/billing/plans/page.tsx | 14 +++++++++++++- .../SettingsSidebar/GeneralSettings.tsx | 16 ++++++++++------ packages/shared/src/constants.ts | 1 + 4 files changed, 36 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx index 18231f0dacc..9dc1679904f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/page.tsx @@ -1,4 +1,6 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { FEATURE_FLAGS } from "@superset/shared/constants"; +import { createFileRoute, Navigate } from "@tanstack/react-router"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { useMemo } from "react"; import { useSettingsSearchQuery } from "renderer/stores/settings-state"; import { getMatchingItemsForSection } from "../utils/settings-search"; @@ -10,6 +12,7 @@ export const Route = createFileRoute("/_authenticated/settings/billing/")({ function BillingPage() { const searchQuery = useSettingsSearchQuery(); + const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); const visibleItems = useMemo(() => { if (!searchQuery) return null; @@ -18,5 +21,13 @@ function BillingPage() { ); }, [searchQuery]); + if (billingEnabled === undefined) { + return null; + } + + if (billingEnabled === false) { + return ; + } + return ; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx index 2e0e2560dd0..dc9697f8b0c 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/billing/plans/page.tsx @@ -1,4 +1,6 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { FEATURE_FLAGS } from "@superset/shared/constants"; +import { createFileRoute, Navigate } from "@tanstack/react-router"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { PlansComparison } from "../components/PlansComparison"; export const Route = createFileRoute("/_authenticated/settings/billing/plans/")( @@ -8,5 +10,15 @@ export const Route = createFileRoute("/_authenticated/settings/billing/plans/")( ); function PlansPage() { + const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); + + if (billingEnabled === undefined) { + return null; + } + + if (billingEnabled === false) { + return ; + } + return ; } diff --git a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx index b22a8810d38..62da88c347a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/settings/components/SettingsSidebar/GeneralSettings.tsx @@ -1,5 +1,7 @@ +import { FEATURE_FLAGS } from "@superset/shared/constants"; import { cn } from "@superset/ui/utils"; import { Link, useMatchRoute } from "@tanstack/react-router"; +import { useFeatureFlagEnabled } from "posthog-js/react"; import { HiOutlineBell, HiOutlineBuildingOffice2, @@ -92,13 +94,15 @@ const GENERAL_SECTIONS: { export function GeneralSettings({ matchCounts }: GeneralSettingsProps) { const matchRoute = useMatchRoute(); + const billingEnabled = useFeatureFlagEnabled(FEATURE_FLAGS.BILLING_ENABLED); - // When searching, only show sections that have matches - const filteredSections = matchCounts - ? GENERAL_SECTIONS.filter( - (section) => (matchCounts[section.section] ?? 0) > 0, - ) - : GENERAL_SECTIONS; + const filteredSections = ( + matchCounts + ? GENERAL_SECTIONS.filter( + (section) => (matchCounts[section.section] ?? 0) > 0, + ) + : GENERAL_SECTIONS + ).filter((section) => section.section !== "billing" || billingEnabled); if (filteredSections.length === 0) { return null; diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 123cb29045e..cc0dcf8646a 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -45,4 +45,5 @@ export const POSTHOG_COOKIE_NAME = "superset"; export const FEATURE_FLAGS = { /** Gates access to experimental Electric SQL tasks feature. */ ELECTRIC_TASKS_ACCESS: "electric-tasks-access", + BILLING_ENABLED: "billing-enabled", } as const; From 6d72c999f6f1188d9a7ecb85c93693722058f603 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 22 Jan 2026 22:12:18 -0800 Subject: [PATCH 2/2] fix(marketing): fix lint errors in hero section components Fix lint issues from PR #910: - Add biome-ignore for useEffect in wave animation - Format code to pass biome checks --- apps/marketing/package.json | 1 + .../components/AppMockup/AppMockup.tsx | 338 ++++++---- .../HeroSection/components/AppMockup/index.ts | 2 +- .../components/ProductDemo/ProductDemo.tsx | 3 +- apps/marketing/src/components/ui/index.ts | 4 +- .../src/components/ui/shader-animation.tsx | 276 ++++---- .../src/components/ui/wave-background.tsx | 612 +++++++++--------- bun.lock | 5 +- 8 files changed, 683 insertions(+), 558 deletions(-) diff --git a/apps/marketing/package.json b/apps/marketing/package.json index bb126f015b3..d21079a80cc 100644 --- a/apps/marketing/package.json +++ b/apps/marketing/package.json @@ -30,6 +30,7 @@ "react-fast-marquee": "^1.6.5", "react-icons": "^5.5.0", "require-in-the-middle": "8.0.1", + "simplex-noise": "^4.0.3", "stripe-gradient": "^1.0.1", "three": "^0.181.2", "zod": "^4.3.5" diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx index 714bfdc8334..6d1b7573956 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/AppMockup.tsx @@ -329,7 +329,9 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
- new workspace + + new workspace +
creating... @@ -339,7 +341,8 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {WORKSPACES.map((ws) => { const isFirstItem = ws.name === "use any agents"; - const shouldHideActiveState = isFirstItem && activeDemo === "Create Parallel Branches"; + const shouldHideActiveState = + isFirstItem && activeDemo === "Create Parallel Branches"; return ( ))}
- {/* Main content area */} @@ -401,7 +403,12 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { ) : ( <> - Claude + Claude claude )} @@ -410,46 +417,88 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Other agent tabs - shown when "Use Any Agents" is active */} - Codex + Codex codex - Gemini + Gemini gemini - Cursor + Cursor cursor @@ -480,93 +529,93 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { }} transition={{ duration: 0.2, ease: "easeOut" }} > - {/* Claude ASCII art header */} -
-
- {` * ▐▛███▜▌ * + {/* Claude ASCII art header */} +
+
+ {` * ▐▛███▜▌ * * ▝▜█████▛▘ * * ▘▘ ▝▝ *`} -
-
-
- - Claude Code - {" "} - v2.0.74
-
Opus 4.5 · Claude Max
-
- ~/.superset/worktrees/superset/cloud-ws +
+
+ + Claude Code + {" "} + v2.0.74 +
+
Opus 4.5 · Claude Max
+
+ ~/.superset/worktrees/superset/cloud-ws +
-
- {/* Command prompt */} -
- {" "} - /mcp -
- - {/* MCP output */} -
-
- - Manage MCP servers - -
-
1 server
- -
- - 1. - morph-mcp - ✓ connected - - · Enter to view details - + {/* Command prompt */} +
+ {" "} + /mcp
-
-
MCP Config locations (by scope):
-
- • User config (available in all your projects): -
-
- · /Users/kietho/.claude.json -
-
- • Project config (shared via .mcp.json): -
-
- · - /Users/kietho/.superset/worktrees/superset/cloud-ws/.mcp.json + {/* MCP output */} +
+
+ + Manage MCP servers +
-
- • Local config (private to you in this project): +
1 server
+ +
+ + 1. + morph-mcp + ✓ connected + + · Enter to view details +
-
- · /Users/kietho/.claude.json [project: ...] + +
+
MCP Config locations (by scope):
+
+ • User config (available in all your projects): +
+
+ · /Users/kietho/.claude.json +
+
+ • Project config (shared via .mcp.json): +
+
+ · + /Users/kietho/.superset/worktrees/superset/cloud-ws/.mcp.json +
+
+ • Local config (private to you in this project): +
+
+ · /Users/kietho/.claude.json [project: ...] +
-
-
-
- Tip: Use /mcp enable or /mcp disable to quickly toggle all - servers +
+
+ Tip: Use /mcp enable or /mcp disable to quickly toggle all + servers +
-
-
- For help configuring MCP servers, see:{" "} - - https://code.claude.com/docs/en/mcp - -
+
+ For help configuring MCP servers, see:{" "} + + https://code.claude.com/docs/en/mcp + +
-
- Enter to confirm · Esc to cancel +
+ Enter to confirm · Esc to cancel +
-
{/* Create Parallel Branches overlay */} @@ -577,7 +626,10 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { opacity: activeDemo === "Create Parallel Branches" ? 1 : 0, }} transition={{ duration: 0.3, ease: "easeOut" }} - style={{ pointerEvents: activeDemo === "Create Parallel Branches" ? "auto" : "none" }} + style={{ + pointerEvents: + activeDemo === "Create Parallel Branches" ? "auto" : "none", + }} >
{" "} @@ -619,11 +671,15 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { opacity: activeDemo === "See Changes" ? 0 : 1, }} transition={{ duration: 0.2, ease: "easeOut" }} - style={{ pointerEvents: activeDemo === "See Changes" ? "none" : "auto" }} + style={{ + pointerEvents: activeDemo === "See Changes" ? "none" : "auto", + }} > {/* Header */}
- Review Changes + + Review Changes +
#827 @@ -674,66 +730,108 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { animate={{ opacity: activeDemo === "See Changes" ? 1 : 0, }} - transition={{ duration: 0.3, ease: "easeOut", delay: activeDemo === "See Changes" ? 0.1 : 0 }} - style={{ pointerEvents: activeDemo === "See Changes" ? "auto" : "none" }} + transition={{ + duration: 0.3, + ease: "easeOut", + delay: activeDemo === "See Changes" ? 0.1 : 0, + }} + style={{ + pointerEvents: activeDemo === "See Changes" ? "auto" : "none", + }} > {/* PR Header */}
- Review PR #827 + + Review PR #827 +
- Open + + Open +
{/* File tabs */}
- cloud-workspace.ts - enums.ts - +4 more + + cloud-workspace.ts + + + enums.ts + + + +4 more +
{/* Diff content */}
-
@@ -1,4 +1,6 @@
+
+ @@ -1,4 +1,6 @@ +
- 1 - import {"{"} db {"}"} from "../db" + + 1 + + + import {"{"} db {"}"} from "../db" +
+ - import {"{"} CloudWorkspace {"}"} from "./types" + + import {"{"} CloudWorkspace {"}"} from "./types" +
+ - import {"{"} createSSHConnection {"}"} from "./ssh" + + import {"{"} createSSHConnection {"}"} from "./ssh" +
- 2 + + 2 +
- - export const getWorkspaces = () ={">"} {"{"} + + export const getWorkspaces = () ={">"} {"{"} +
+ - export const getWorkspaces = async () ={">"} {"{"} + + export const getWorkspaces = async () ={">"} {"{"} +
- 4 - {" "}return db.query.workspaces + + 4 + + + {" "}return db.query.workspaces +
{/* Review actions */}
- -
@@ -790,12 +888,30 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) { {/* Code editor */}
-
import {"{"} Agent {"}"} from "ai"
-
import {"{"} tools {"}"} from "./utils"
+
+ import {"{"} Agent{" "} + {"}"} from{" "} + "ai" +
+
+ import {"{"} tools{" "} + {"}"} from{" "} + "./utils" +
-
const agent = new Agent({"{"}
-
model: "claude-4",
-
tools: [tools.read, tools.write]
+
+ const{" "} + agent ={" "} + new Agent({"{"} +
+
+ model:{" "} + "claude-4", +
+
+ tools: [tools.read, + tools.write] +
{"}"})
diff --git a/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts b/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts index fab8b601a45..8457eed8da5 100644 --- a/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts +++ b/apps/marketing/src/app/components/HeroSection/components/AppMockup/index.ts @@ -1 +1 @@ -export { AppMockup, type ActiveDemo } from "./AppMockup"; +export { type ActiveDemo, AppMockup } from "./AppMockup"; diff --git a/apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx b/apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx index 56351fbc5e6..8c58dc577b0 100644 --- a/apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx +++ b/apps/marketing/src/app/components/HeroSection/components/ProductDemo/ProductDemo.tsx @@ -8,7 +8,8 @@ import { SelectorPill } from "./components/SelectorPill"; import { DEMO_OPTIONS } from "./constants"; export function ProductDemo() { - const [activeOption, setActiveOption] = useState("Use Any Agents"); + const [activeOption, setActiveOption] = + useState("Use Any Agents"); return (
diff --git a/apps/marketing/src/components/ui/index.ts b/apps/marketing/src/components/ui/index.ts index d146457b106..be1120f15bf 100644 --- a/apps/marketing/src/components/ui/index.ts +++ b/apps/marketing/src/components/ui/index.ts @@ -1,2 +1,2 @@ -export { ShaderAnimation } from "./shader-animation" -export { Waves } from "./wave-background" +export { ShaderAnimation } from "./shader-animation"; +export { Waves } from "./wave-background"; diff --git a/apps/marketing/src/components/ui/shader-animation.tsx b/apps/marketing/src/components/ui/shader-animation.tsx index 5ee37a60d9d..ec507b0409f 100644 --- a/apps/marketing/src/components/ui/shader-animation.tsx +++ b/apps/marketing/src/components/ui/shader-animation.tsx @@ -1,47 +1,47 @@ -"use client" +"use client"; -import { useEffect, useRef } from "react" -import * as THREE from "three" +import { useEffect, useRef } from "react"; +import * as THREE from "three"; interface ShaderAnimationProps { - className?: string - opacity?: number - speed?: number - intensity?: number + className?: string; + opacity?: number; + speed?: number; + intensity?: number; } export function ShaderAnimation({ - className = "", - opacity = 0.15, - speed = 0.008, - intensity = 0.0003, + className = "", + opacity = 0.15, + speed = 0.008, + intensity = 0.0003, }: ShaderAnimationProps) { - const containerRef = useRef(null) - const sceneRef = useRef<{ - camera: THREE.Camera - scene: THREE.Scene - renderer: THREE.WebGLRenderer - uniforms: { - time: { type: string; value: number } - resolution: { type: string; value: THREE.Vector2 } - intensity: { type: string; value: number } - } - animationId: number - startTime: number - } | null>(null) - - useEffect(() => { - if (!containerRef.current) return - - const container = containerRef.current - - const vertexShader = ` + const containerRef = useRef(null); + const sceneRef = useRef<{ + camera: THREE.Camera; + scene: THREE.Scene; + renderer: THREE.WebGLRenderer; + uniforms: { + time: { type: string; value: number }; + resolution: { type: string; value: THREE.Vector2 }; + intensity: { type: string; value: number }; + }; + animationId: number; + startTime: number; + } | null>(null); + + useEffect(() => { + if (!containerRef.current) return; + + const container = containerRef.current; + + const vertexShader = ` void main() { gl_Position = vec4(position, 1.0); } - ` + `; - const fragmentShader = ` + const fragmentShader = ` #define TWO_PI 6.2831853072 #define PI 3.14159265359 @@ -64,109 +64,109 @@ export function ShaderAnimation({ gl_FragColor = vec4(color[0], color[1], color[2], 1.0); } - ` - - const camera = new THREE.Camera() - camera.position.z = 1 - - const scene = new THREE.Scene() - const geometry = new THREE.PlaneGeometry(2, 2) - - const uniforms = { - time: { type: "f", value: 1.0 }, - resolution: { type: "v2", value: new THREE.Vector2() }, - intensity: { type: "f", value: intensity }, - } - - const material = new THREE.ShaderMaterial({ - uniforms: uniforms, - vertexShader: vertexShader, - fragmentShader: fragmentShader, - }) - - const mesh = new THREE.Mesh(geometry, material) - scene.add(mesh) - - const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true }) - renderer.setPixelRatio(1) - renderer.setClearColor(0x000000, 0) - - container.appendChild(renderer.domElement) - - const onWindowResize = () => { - const width = container.clientWidth - const height = container.clientHeight - renderer.setSize(width, height) - uniforms.resolution.value.x = renderer.domElement.width - uniforms.resolution.value.y = renderer.domElement.height - } - - onWindowResize() - window.addEventListener("resize", onWindowResize, false) - - const startTime = performance.now() - let lastRenderTime = 0 - const targetFPS = 10 - const frameInterval = 1000 / targetFPS - - const animate = (currentTime: number) => { - const animationId = requestAnimationFrame(animate) - - if (currentTime - lastRenderTime < frameInterval) { - if (sceneRef.current) { - sceneRef.current.animationId = animationId - } - return - } - lastRenderTime = currentTime - - const elapsed = (performance.now() - startTime) * 0.001 - const oscillation = Math.sin(elapsed * speed) * 6 - uniforms.time.value = oscillation - - renderer.render(scene, camera) - - if (sceneRef.current) { - sceneRef.current.animationId = animationId - } - } - - sceneRef.current = { - camera, - scene, - renderer, - uniforms, - animationId: 0, - startTime, - } - - requestAnimationFrame(animate) - - return () => { - window.removeEventListener("resize", onWindowResize) - - if (sceneRef.current) { - cancelAnimationFrame(sceneRef.current.animationId) - - if (container && sceneRef.current.renderer.domElement) { - container.removeChild(sceneRef.current.renderer.domElement) - } - - sceneRef.current.renderer.dispose() - geometry.dispose() - material.dispose() - } - } - }, [speed, intensity]) - - return ( -
- ) + `; + + const camera = new THREE.Camera(); + camera.position.z = 1; + + const scene = new THREE.Scene(); + const geometry = new THREE.PlaneGeometry(2, 2); + + const uniforms = { + time: { type: "f", value: 1.0 }, + resolution: { type: "v2", value: new THREE.Vector2() }, + intensity: { type: "f", value: intensity }, + }; + + const material = new THREE.ShaderMaterial({ + uniforms: uniforms, + vertexShader: vertexShader, + fragmentShader: fragmentShader, + }); + + const mesh = new THREE.Mesh(geometry, material); + scene.add(mesh); + + const renderer = new THREE.WebGLRenderer({ antialias: false, alpha: true }); + renderer.setPixelRatio(1); + renderer.setClearColor(0x000000, 0); + + container.appendChild(renderer.domElement); + + const onWindowResize = () => { + const width = container.clientWidth; + const height = container.clientHeight; + renderer.setSize(width, height); + uniforms.resolution.value.x = renderer.domElement.width; + uniforms.resolution.value.y = renderer.domElement.height; + }; + + onWindowResize(); + window.addEventListener("resize", onWindowResize, false); + + const startTime = performance.now(); + let lastRenderTime = 0; + const targetFPS = 10; + const frameInterval = 1000 / targetFPS; + + const animate = (currentTime: number) => { + const animationId = requestAnimationFrame(animate); + + if (currentTime - lastRenderTime < frameInterval) { + if (sceneRef.current) { + sceneRef.current.animationId = animationId; + } + return; + } + lastRenderTime = currentTime; + + const elapsed = (performance.now() - startTime) * 0.001; + const oscillation = Math.sin(elapsed * speed) * 6; + uniforms.time.value = oscillation; + + renderer.render(scene, camera); + + if (sceneRef.current) { + sceneRef.current.animationId = animationId; + } + }; + + sceneRef.current = { + camera, + scene, + renderer, + uniforms, + animationId: 0, + startTime, + }; + + requestAnimationFrame(animate); + + return () => { + window.removeEventListener("resize", onWindowResize); + + if (sceneRef.current) { + cancelAnimationFrame(sceneRef.current.animationId); + + if (container && sceneRef.current.renderer.domElement) { + container.removeChild(sceneRef.current.renderer.domElement); + } + + sceneRef.current.renderer.dispose(); + geometry.dispose(); + material.dispose(); + } + }; + }, [speed, intensity]); + + return ( +
+ ); } diff --git a/apps/marketing/src/components/ui/wave-background.tsx b/apps/marketing/src/components/ui/wave-background.tsx index 88e3fc35981..b1650a59479 100644 --- a/apps/marketing/src/components/ui/wave-background.tsx +++ b/apps/marketing/src/components/ui/wave-background.tsx @@ -1,314 +1,318 @@ -'use client' -import * as React from 'react' -import { useEffect, useRef } from 'react' -import { createNoise2D } from 'simplex-noise' +"use client"; +import type * as React from "react"; +import { useEffect, useRef } from "react"; +import { createNoise2D } from "simplex-noise"; interface Point { - x: number - y: number - wave: { x: number; y: number } - cursor: { - x: number - y: number - vx: number - vy: number - } + x: number; + y: number; + wave: { x: number; y: number }; + cursor: { + x: number; + y: number; + vx: number; + vy: number; + }; } interface WavesProps { - className?: string - strokeColor?: string - backgroundColor?: string - pointerSize?: number + className?: string; + strokeColor?: string; + backgroundColor?: string; + pointerSize?: number; } export function Waves({ - className = "", - strokeColor = "#ffffff", // White lines - backgroundColor = "#000000", // Black background - pointerSize = 0.5 + className = "", + strokeColor = "#ffffff", // White lines + backgroundColor = "#000000", // Black background + pointerSize = 0.5, }: WavesProps) { - const containerRef = useRef(null) - const svgRef = useRef(null) - const mouseRef = useRef({ - x: -10, - y: 0, - lx: 0, - ly: 0, - sx: 0, - sy: 0, - v: 0, - vs: 0, - a: 0, - set: false, - }) - const pathsRef = useRef([]) - const linesRef = useRef([]) - const noiseRef = useRef<((x: number, y: number) => number) | null>(null) - const rafRef = useRef(null) - const boundingRef = useRef(null) - - useEffect(() => { - if (!containerRef.current || !svgRef.current) return - - noiseRef.current = createNoise2D() - - setSize() - setLines() - - window.addEventListener('resize', onResize) - window.addEventListener('mousemove', onMouseMove) - containerRef.current.addEventListener('touchmove', onTouchMove, { passive: false }) - - rafRef.current = requestAnimationFrame(tick) - - return () => { - if (rafRef.current) cancelAnimationFrame(rafRef.current) - window.removeEventListener('resize', onResize) - window.removeEventListener('mousemove', onMouseMove) - containerRef.current?.removeEventListener('touchmove', onTouchMove) - } - }, []) - - const setSize = () => { - if (!containerRef.current || !svgRef.current) return - - boundingRef.current = containerRef.current.getBoundingClientRect() - const { width, height } = boundingRef.current - - svgRef.current.style.width = `${width}px` - svgRef.current.style.height = `${height}px` - } - - const setLines = () => { - if (!svgRef.current || !boundingRef.current) return - - const { width, height } = boundingRef.current - linesRef.current = [] - - pathsRef.current.forEach(path => { - path.remove() - }) - pathsRef.current = [] - - const xGap = 8 - const yGap = 8 - - const oWidth = width + 200 - const oHeight = height + 30 - - const totalLines = Math.ceil(oWidth / xGap) - const totalPoints = Math.ceil(oHeight / yGap) - - const xStart = (width - xGap * totalLines) / 2 - const yStart = (height - yGap * totalPoints) / 2 - - for (let i = 0; i < totalLines; i++) { - const points: Point[] = [] - - for (let j = 0; j < totalPoints; j++) { - const point: Point = { - x: xStart + xGap * i, - y: yStart + yGap * j, - wave: { x: 0, y: 0 }, - cursor: { x: 0, y: 0, vx: 0, vy: 0 }, - } - - points.push(point) - } - - const path = document.createElementNS( - 'http://www.w3.org/2000/svg', - 'path' - ) - path.classList.add('a__line') - path.classList.add('js-line') - path.setAttribute('fill', 'none') - path.setAttribute('stroke', strokeColor) - path.setAttribute('stroke-width', '1') - - svgRef.current.appendChild(path) - pathsRef.current.push(path) - - linesRef.current.push(points) - } - } - - const onResize = () => { - setSize() - setLines() - } - - const onMouseMove = (e: MouseEvent) => { - updateMousePosition(e.pageX, e.pageY) - } - - const onTouchMove = (e: TouchEvent) => { - e.preventDefault() - const touch = e.touches[0] - if (touch) { - updateMousePosition(touch.clientX, touch.clientY) - } - } - - const updateMousePosition = (x: number, y: number) => { - if (!boundingRef.current) return - - const mouse = mouseRef.current - mouse.x = x - boundingRef.current.left - mouse.y = y - boundingRef.current.top + window.scrollY - - if (!mouse.set) { - mouse.sx = mouse.x - mouse.sy = mouse.y - mouse.lx = mouse.x - mouse.ly = mouse.y - - mouse.set = true - } - - if (containerRef.current) { - containerRef.current.style.setProperty('--x', `${mouse.sx}px`) - containerRef.current.style.setProperty('--y', `${mouse.sy}px`) - } - } - - const movePoints = (time: number) => { - const { current: lines } = linesRef - const { current: mouse } = mouseRef - const { current: noise } = noiseRef - - if (!noise) return - - lines.forEach((points) => { - points.forEach((p: Point) => { - const move = noise( - (p.x + time * 0.002) * 0.002, - (p.y + time * 0.001) * 0.001 - ) * 3 - - p.wave.x = Math.cos(move) * 3 - p.wave.y = Math.sin(move) * 1.5 - - const dx = p.x - mouse.sx - const dy = p.y - mouse.sy - const d = Math.hypot(dx, dy) - const l = 100 - - let targetX = 0 - let targetY = 0 - - if (d < l && d > 0) { - const s = (1 - d / l) * (1 - d / l) - targetX = (dx / d) * s * 10 - targetY = (dy / d) * s * 10 - } - - p.cursor.x += (targetX - p.cursor.x) * 0.3 - p.cursor.y += (targetY - p.cursor.y) * 0.3 - }) - }) - } - - const moved = (point: Point, withCursorForce = true) => { - const coords = { - x: point.x + point.wave.x + (withCursorForce ? point.cursor.x : 0), - y: point.y + point.wave.y + (withCursorForce ? point.cursor.y : 0), - } - - return coords - } - - const drawLines = () => { - const { current: lines } = linesRef - const { current: paths } = pathsRef - - lines.forEach((points, lIndex) => { - const path = paths[lIndex] - const first = points[0] - if (points.length < 2 || !path || !first) return; - - const firstPoint = moved(first, false) - let d = `M ${firstPoint.x} ${firstPoint.y}` - - for (let i = 1; i < points.length; i++) { - const point = points[i] - if (!point) continue - const current = moved(point) - d += `L ${current.x} ${current.y}` - } - - path.setAttribute('d', d) - }) - } - - const tick = (time: number) => { - const { current: mouse } = mouseRef - - mouse.sx += (mouse.x - mouse.sx) * 0.1 - mouse.sy += (mouse.y - mouse.sy) * 0.1 - - const dx = mouse.x - mouse.lx - const dy = mouse.y - mouse.ly - const d = Math.hypot(dx, dy) - - mouse.v = d - mouse.vs += (d - mouse.vs) * 0.1 - mouse.vs = Math.min(100, mouse.vs) - - mouse.lx = mouse.x - mouse.ly = mouse.y - - mouse.a = Math.atan2(dy, dx) - - if (containerRef.current) { - containerRef.current.style.setProperty('--x', `${mouse.sx}px`) - containerRef.current.style.setProperty('--y', `${mouse.sy}px`) - } - - movePoints(time) - drawLines() - - rafRef.current = requestAnimationFrame(tick) - } - - return ( -
- -
-
- ) + const containerRef = useRef(null); + const svgRef = useRef(null); + const mouseRef = useRef({ + x: -10, + y: 0, + lx: 0, + ly: 0, + sx: 0, + sy: 0, + v: 0, + vs: 0, + a: 0, + set: false, + }); + const pathsRef = useRef([]); + const linesRef = useRef([]); + const noiseRef = useRef<((x: number, y: number) => number) | null>(null); + const rafRef = useRef(null); + const boundingRef = useRef(null); + + // biome-ignore lint/correctness/useExhaustiveDependencies: Animation runs once on mount + useEffect(() => { + if (!containerRef.current || !svgRef.current) return; + + noiseRef.current = createNoise2D(); + + setSize(); + setLines(); + + window.addEventListener("resize", onResize); + window.addEventListener("mousemove", onMouseMove); + containerRef.current.addEventListener("touchmove", onTouchMove, { + passive: false, + }); + + rafRef.current = requestAnimationFrame(tick); + + return () => { + if (rafRef.current) cancelAnimationFrame(rafRef.current); + window.removeEventListener("resize", onResize); + window.removeEventListener("mousemove", onMouseMove); + containerRef.current?.removeEventListener("touchmove", onTouchMove); + }; + }, []); + + const setSize = () => { + if (!containerRef.current || !svgRef.current) return; + + boundingRef.current = containerRef.current.getBoundingClientRect(); + const { width, height } = boundingRef.current; + + svgRef.current.style.width = `${width}px`; + svgRef.current.style.height = `${height}px`; + }; + + const setLines = () => { + if (!svgRef.current || !boundingRef.current) return; + + const { width, height } = boundingRef.current; + linesRef.current = []; + + pathsRef.current.forEach((path) => { + path.remove(); + }); + pathsRef.current = []; + + const xGap = 8; + const yGap = 8; + + const oWidth = width + 200; + const oHeight = height + 30; + + const totalLines = Math.ceil(oWidth / xGap); + const totalPoints = Math.ceil(oHeight / yGap); + + const xStart = (width - xGap * totalLines) / 2; + const yStart = (height - yGap * totalPoints) / 2; + + for (let i = 0; i < totalLines; i++) { + const points: Point[] = []; + + for (let j = 0; j < totalPoints; j++) { + const point: Point = { + x: xStart + xGap * i, + y: yStart + yGap * j, + wave: { x: 0, y: 0 }, + cursor: { x: 0, y: 0, vx: 0, vy: 0 }, + }; + + points.push(point); + } + + const path = document.createElementNS( + "http://www.w3.org/2000/svg", + "path", + ); + path.classList.add("a__line"); + path.classList.add("js-line"); + path.setAttribute("fill", "none"); + path.setAttribute("stroke", strokeColor); + path.setAttribute("stroke-width", "1"); + + svgRef.current.appendChild(path); + pathsRef.current.push(path); + + linesRef.current.push(points); + } + }; + + const onResize = () => { + setSize(); + setLines(); + }; + + const onMouseMove = (e: MouseEvent) => { + updateMousePosition(e.pageX, e.pageY); + }; + + const onTouchMove = (e: TouchEvent) => { + e.preventDefault(); + const touch = e.touches[0]; + if (touch) { + updateMousePosition(touch.clientX, touch.clientY); + } + }; + + const updateMousePosition = (x: number, y: number) => { + if (!boundingRef.current) return; + + const mouse = mouseRef.current; + mouse.x = x - boundingRef.current.left; + mouse.y = y - boundingRef.current.top + window.scrollY; + + if (!mouse.set) { + mouse.sx = mouse.x; + mouse.sy = mouse.y; + mouse.lx = mouse.x; + mouse.ly = mouse.y; + + mouse.set = true; + } + + if (containerRef.current) { + containerRef.current.style.setProperty("--x", `${mouse.sx}px`); + containerRef.current.style.setProperty("--y", `${mouse.sy}px`); + } + }; + + const movePoints = (time: number) => { + const { current: lines } = linesRef; + const { current: mouse } = mouseRef; + const { current: noise } = noiseRef; + + if (!noise) return; + + lines.forEach((points) => { + points.forEach((p: Point) => { + const move = + noise((p.x + time * 0.002) * 0.002, (p.y + time * 0.001) * 0.001) * 3; + + p.wave.x = Math.cos(move) * 3; + p.wave.y = Math.sin(move) * 1.5; + + const dx = p.x - mouse.sx; + const dy = p.y - mouse.sy; + const d = Math.hypot(dx, dy); + const l = 100; + + let targetX = 0; + let targetY = 0; + + if (d < l && d > 0) { + const s = (1 - d / l) * (1 - d / l); + targetX = (dx / d) * s * 10; + targetY = (dy / d) * s * 10; + } + + p.cursor.x += (targetX - p.cursor.x) * 0.3; + p.cursor.y += (targetY - p.cursor.y) * 0.3; + }); + }); + }; + + const moved = (point: Point, withCursorForce = true) => { + const coords = { + x: point.x + point.wave.x + (withCursorForce ? point.cursor.x : 0), + y: point.y + point.wave.y + (withCursorForce ? point.cursor.y : 0), + }; + + return coords; + }; + + const drawLines = () => { + const { current: lines } = linesRef; + const { current: paths } = pathsRef; + + lines.forEach((points, lIndex) => { + const path = paths[lIndex]; + const first = points[0]; + if (points.length < 2 || !path || !first) return; + + const firstPoint = moved(first, false); + let d = `M ${firstPoint.x} ${firstPoint.y}`; + + for (let i = 1; i < points.length; i++) { + const point = points[i]; + if (!point) continue; + const current = moved(point); + d += `L ${current.x} ${current.y}`; + } + + path.setAttribute("d", d); + }); + }; + + const tick = (time: number) => { + const { current: mouse } = mouseRef; + + mouse.sx += (mouse.x - mouse.sx) * 0.1; + mouse.sy += (mouse.y - mouse.sy) * 0.1; + + const dx = mouse.x - mouse.lx; + const dy = mouse.y - mouse.ly; + const d = Math.hypot(dx, dy); + + mouse.v = d; + mouse.vs += (d - mouse.vs) * 0.1; + mouse.vs = Math.min(100, mouse.vs); + + mouse.lx = mouse.x; + mouse.ly = mouse.y; + + mouse.a = Math.atan2(dy, dx); + + if (containerRef.current) { + containerRef.current.style.setProperty("--x", `${mouse.sx}px`); + containerRef.current.style.setProperty("--y", `${mouse.sy}px`); + } + + movePoints(time); + drawLines(); + + rafRef.current = requestAnimationFrame(tick); + }; + + return ( +
+ +
+
+ ); } diff --git a/bun.lock b/bun.lock index 760de805158..6dc27a12393 100644 --- a/bun.lock +++ b/bun.lock @@ -125,7 +125,7 @@ }, "apps/desktop": { "name": "@superset/desktop", - "version": "0.0.60", + "version": "0.0.61", "dependencies": { "@better-auth/stripe": "^1.4.17", "@dnd-kit/core": "^6.3.1", @@ -318,6 +318,7 @@ "react-fast-marquee": "^1.6.5", "react-icons": "^5.5.0", "require-in-the-middle": "8.0.1", + "simplex-noise": "^4.0.3", "stripe-gradient": "^1.0.1", "three": "^0.181.2", "zod": "^4.3.5", @@ -4233,6 +4234,8 @@ "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], + "simplex-noise": ["simplex-noise@4.0.3", "", {}, "sha512-qSE2I4AngLQG7BXqoZj51jokT4WUXe8mOBrvfOXpci8+6Yu44+/dD5zqDpOx3Ux792eamTd2lLcI8jqFntk/lg=="], + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],