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/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
>
)}
@@ -410,46 +417,88 @@ export function AppMockup({ activeDemo = "Use Any Agents" }: AppMockupProps) {
{/* Other agent tabs - shown when "Use Any Agents" is active */}
-
+
codex
-
+
gemini
-
+
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=="],
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;