@checkstack/ui 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.3.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 0388000: Implemented a global performance-aware UI infrastructure that detects hardware capabilities (using heuristics and frame-budget benchmarks) to automatically disable expensive CSS animations, backdrop-blurs, and glassmorphism effects on low-power or non-hardware-accelerated devices.
8
+
9
+ ## 1.3.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 765b764: Optimize AmbientBackground performance by replacing thousand-div grid with a single-element CSS mask and hardware-accelerated Aurora Mesh animations.
14
+
3
15
  ## 1.3.0
4
16
 
5
17
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
@@ -1,104 +1,95 @@
1
- import React, { useMemo, useEffect, useState } from "react";
1
+ import React, { useMemo } from "react";
2
2
  import { cn } from "../utils";
3
+ import { usePerformance } from "./PerformanceProvider";
3
4
 
4
5
  interface AmbientBackgroundProps {
5
6
  children: React.ReactNode;
6
7
  className?: string;
7
8
  }
8
9
 
9
- const TILE_SIZE = 48;
10
-
11
10
  /**
12
- * AmbientBackground - Animated checkerboard pattern
13
- * Features a chess-inspired grid where random tiles glow with the primary color.
14
- * Dynamically adapts to screen size.
11
+ * AmbientBackground - Premium background with performance-aware fallbacks.
12
+ * Features an "Inverse Glow Grid" where aurora effects shine through transparency.
13
+ * Automatically downgrades to a static grid on devices that struggle with CSS blurs.
15
14
  */
16
15
  export const AmbientBackground: React.FC<AmbientBackgroundProps> = ({
17
16
  children,
18
17
  className,
19
18
  }) => {
20
- const [dimensions, setDimensions] = useState({ cols: 40, rows: 25 });
21
-
22
- // Calculate grid size based on viewport
23
- useEffect(() => {
24
- const updateDimensions = () => {
25
- const cols = Math.ceil(globalThis.innerWidth / TILE_SIZE) + 2;
26
- const rows = Math.ceil(globalThis.innerHeight / TILE_SIZE) + 2;
27
- setDimensions({ cols, rows });
28
- };
29
-
30
- updateDimensions();
31
- globalThis.addEventListener("resize", updateDimensions);
32
- return () => globalThis.removeEventListener("resize", updateDimensions);
33
- }, []);
34
-
35
- // Generate tile grid with staggered animation delays
36
- const tiles = useMemo(() => {
37
- const { cols, rows } = dimensions;
38
- const result: Array<{ key: string; delay: number; isLight: boolean }> = [];
19
+ const { isLowPower } = usePerformance();
39
20
 
40
- for (let row = 0; row < rows; row++) {
41
- for (let col = 0; col < cols; col++) {
42
- const isLight = (row + col) % 2 === 0;
43
- // Negative delay so tiles start at different points in their 12s cycle
44
- // This creates a gentle, staggered breathing effect across the grid
45
- const delay = -((row * 7 + col * 13 + row * col) % 24) * 0.5;
46
- result.push({
47
- key: `${row}-${col}`,
48
- delay,
49
- isLight,
50
- });
51
- }
52
- }
53
- return result;
54
- }, [dimensions]);
21
+ // Optimized Aurora Layers - only render if not in low power mode
22
+ const auroraBlobs = useMemo(() => {
23
+ if (isLowPower) return;
24
+ return (
25
+ <>
26
+ <div
27
+ className="aurora-blob absolute w-[50%] h-[50%] -top-[10%] -left-[10%]"
28
+ style={{
29
+ background:
30
+ "radial-gradient(circle at center, hsl(var(--primary) / 0.8), transparent 60%)",
31
+ animation: "aurora-float-1 25s ease-in-out infinite",
32
+ }}
33
+ />
34
+ <div
35
+ className="aurora-blob absolute w-[40%] h-[40%] bottom-[10%] right-[10%]"
36
+ style={{
37
+ background:
38
+ "radial-gradient(circle at center, hsl(var(--chart-2) / 0.7), transparent 60%)",
39
+ animation: "aurora-float-2 20s ease-in-out infinite",
40
+ }}
41
+ />
42
+ <div
43
+ className="aurora-blob absolute w-[35%] h-[35%] top-[30%] left-[40%]"
44
+ style={{
45
+ background:
46
+ "radial-gradient(circle at center, hsl(var(--primary) / 0.6), transparent 60%)",
47
+ animation: "aurora-float-3 30s ease-in-out infinite",
48
+ }}
49
+ />
50
+ <div
51
+ className="aurora-blob absolute w-[45%] h-[45%] bottom-[20%] left-[10%]"
52
+ style={{
53
+ background:
54
+ "radial-gradient(circle at center, hsl(var(--chart-1) / 0.5), transparent 60%)",
55
+ animation: "aurora-float-4 35s ease-in-out infinite",
56
+ }}
57
+ />
58
+ </>
59
+ );
60
+ }, [isLowPower]);
55
61
 
56
62
  return (
57
63
  <div
58
64
  className={cn(
59
65
  "relative min-h-screen bg-background overflow-hidden",
60
- className
66
+ className,
61
67
  )}
62
68
  >
63
- {/* Checkerboard grid - covers full viewport */}
64
- <div
65
- className="pointer-events-none fixed inset-0 overflow-hidden"
66
- style={{
67
- display: "grid",
68
- gridTemplateColumns: `repeat(${dimensions.cols}, ${TILE_SIZE}px)`,
69
- gridTemplateRows: `repeat(${dimensions.rows}, ${TILE_SIZE}px)`,
70
- }}
71
- >
72
- {tiles.map(({ key, delay, isLight }) => (
73
- <div
74
- key={key}
75
- className="tile-glow"
76
- style={
77
- {
78
- width: TILE_SIZE,
79
- height: TILE_SIZE,
80
- backgroundColor: isLight
81
- ? "hsl(var(--muted-foreground) / 0.12)"
82
- : "hsl(var(--muted-foreground) / 0.04)",
83
- "--glow-delay": `${delay}s`,
84
- } as React.CSSProperties
85
- }
86
- />
87
- ))}
69
+ <div className="pointer-events-none fixed inset-0 overflow-hidden">
70
+ {/* Layer 1: Aurora Blobs (Bottom) */}
71
+ {auroraBlobs}
72
+
73
+ {/* Layer 2: Grid Mask - Switches mode based on performance capability */}
74
+ <div
75
+ className={cn(
76
+ isLowPower ? "ambient-grid" : "ambient-grid-inverse",
77
+ "absolute inset-0 overflow-hidden",
78
+ )}
79
+ />
88
80
  </div>
89
81
 
90
- {/* Edge vignette to fade out the grid smoothly */}
82
+ {/* Layer 3: Edge vignette fade */}
91
83
  <div
92
84
  className="pointer-events-none fixed inset-0"
93
85
  style={{
94
86
  background: `
95
- linear-gradient(to right, hsl(var(--background)) 0%, transparent 5%, transparent 95%, hsl(var(--background)) 100%),
96
- linear-gradient(to bottom, hsl(var(--background)) 0%, transparent 8%, transparent 92%, hsl(var(--background)) 100%)
87
+ linear-gradient(to right, hsl(var(--background)) 0%, transparent 15%, transparent 85%, hsl(var(--background)) 100%),
88
+ linear-gradient(to bottom, hsl(var(--background)) 0%, transparent 15%, transparent 85%, hsl(var(--background)) 100%)
97
89
  `,
98
90
  }}
99
91
  />
100
92
 
101
- {/* Content */}
102
93
  <div className="relative z-10">{children}</div>
103
94
  </div>
104
95
  );
@@ -1,4 +1,5 @@
1
1
  import React, { useEffect, useState, useRef } from "react";
2
+ import { usePerformance } from "./PerformanceProvider";
2
3
 
3
4
  interface AnimatedCounterProps {
4
5
  value: number;
@@ -20,15 +21,17 @@ export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({
20
21
  const [displayValue, setDisplayValue] = useState(0);
21
22
  const displayValueRef = useRef(displayValue);
22
23
  displayValueRef.current = displayValue;
24
+
25
+ const { isLowPower } = usePerformance();
23
26
 
24
27
  useEffect(() => {
25
- // Skip animation if value is 0 or duration is 0
26
- if (value === 0 || duration === 0) {
28
+ // Skip animation if value is 0, duration is 0, or in low-power mode
29
+ if (value === 0 || duration === 0 || isLowPower) {
27
30
  setDisplayValue(value);
28
31
  return;
29
32
  }
30
33
 
31
- const startTime = performance.now();
34
+ const startTime = globalThis.performance.now();
32
35
  const startValue = displayValueRef.current;
33
36
  const diff = value - startValue;
34
37
 
@@ -48,7 +51,7 @@ export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({
48
51
  };
49
52
 
50
53
  requestAnimationFrame(animate);
51
- }, [value, duration]);
54
+ }, [value, duration, isLowPower]);
52
55
 
53
56
  return <span className={className}>{formatter(displayValue)}</span>;
54
57
  };
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { cn } from "../utils";
3
3
  import { Command } from "lucide-react";
4
+ import { usePerformance } from "./PerformanceProvider";
4
5
 
5
6
  interface CommandPaletteProps {
6
7
  onClick?: () => void;
@@ -17,6 +18,8 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({
17
18
  placeholder = "Search systems, incidents, or run commands...",
18
19
  className,
19
20
  }) => {
21
+ const { isLowPower } = usePerformance();
22
+
20
23
  return (
21
24
  <button
22
25
  onClick={onClick}
@@ -24,18 +27,20 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({
24
27
  // Base styles
25
28
  "w-full flex items-center gap-3 px-4 py-3 rounded-xl",
26
29
  // Glassmorphism effect
27
- "bg-card/50 backdrop-blur-sm border border-border/50",
30
+ isLowPower ? "bg-card" : "bg-card/50 backdrop-blur-sm",
31
+ "border border-border/50",
28
32
  // Glow and shadow
29
33
  "shadow-lg shadow-primary/5 hover:shadow-xl hover:shadow-primary/10",
30
34
  // Hover state
31
35
  "hover:border-primary/30 hover:bg-card/70",
32
36
  // Transition
33
- "transition-all duration-300 ease-out",
37
+ "transition-all",
38
+ !isLowPower && "duration-300 ease-out",
34
39
  // Focus ring
35
40
  "focus:outline-none focus:ring-2 focus:ring-primary/50 focus:ring-offset-2 focus:ring-offset-background",
36
41
  // Cursor
37
42
  "cursor-text",
38
- className
43
+ className,
39
44
  )}
40
45
  >
41
46
  {/* Search icon */}
@@ -63,7 +68,7 @@ export const CommandPalette: React.FC<CommandPaletteProps> = ({
63
68
  className={cn(
64
69
  "hidden sm:flex items-center gap-1 px-2 py-1 rounded-md",
65
70
  "bg-muted/50 border border-border/50",
66
- "text-xs text-muted-foreground font-mono"
71
+ "text-xs text-muted-foreground font-mono",
67
72
  )}
68
73
  >
69
74
  <Command className="w-3 h-3" />
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
  import * as DialogPrimitive from "@radix-ui/react-dialog";
3
3
  import { cva, type VariantProps } from "class-variance-authority";
4
4
  import { cn } from "../utils";
5
+ import { usePerformance } from "./PerformanceProvider";
5
6
 
6
7
  const Dialog = DialogPrimitive.Root;
7
8
 
@@ -14,20 +15,24 @@ const DialogClose = DialogPrimitive.Close;
14
15
  const DialogOverlay = React.forwardRef<
15
16
  React.ElementRef<typeof DialogPrimitive.Overlay>,
16
17
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
17
- >(({ className, ...props }, ref) => (
18
- <DialogPrimitive.Overlay
19
- ref={ref}
20
- className={cn(
21
- "fixed inset-0 z-50 bg-black/50 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
22
- className,
23
- )}
24
- {...props}
25
- />
26
- ));
18
+ >(({ className, ...props }, ref) => {
19
+ const { isLowPower } = usePerformance();
20
+ return (
21
+ <DialogPrimitive.Overlay
22
+ ref={ref}
23
+ className={cn(
24
+ "fixed inset-0 z-50 bg-black/50",
25
+ !isLowPower && "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
26
+ className,
27
+ )}
28
+ {...props}
29
+ />
30
+ );
31
+ });
27
32
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
28
33
 
29
34
  const dialogContentVariants = cva(
30
- "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background text-foreground p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg max-h-[85vh] overflow-y-auto overflow-x-visible",
35
+ "fixed left-[50%] top-[50%] z-50 grid w-full translate-x-[-50%] translate-y-[-50%] gap-4 border border-border bg-background text-foreground p-6 shadow-lg sm:rounded-lg max-h-[85vh] overflow-y-auto overflow-x-visible",
31
36
  {
32
37
  variants: {
33
38
  size: {
@@ -52,19 +57,26 @@ interface DialogContentProps
52
57
  const DialogContent = React.forwardRef<
53
58
  React.ElementRef<typeof DialogPrimitive.Content>,
54
59
  DialogContentProps
55
- >(({ className, children, size, ...props }, ref) => (
56
- <DialogPortal>
57
- <DialogOverlay />
58
- <DialogPrimitive.Content
59
- ref={ref}
60
- className={cn(dialogContentVariants({ size }), className)}
61
- {...props}
62
- >
60
+ >(({ className, children, size, ...props }, ref) => {
61
+ const { isLowPower } = usePerformance();
62
+ return (
63
+ <DialogPortal>
64
+ <DialogOverlay />
65
+ <DialogPrimitive.Content
66
+ ref={ref}
67
+ className={cn(
68
+ dialogContentVariants({ size }),
69
+ !isLowPower && "duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
70
+ className
71
+ )}
72
+ {...props}
73
+ >
63
74
  {/* Wrapper with negative margin and positive padding to allow focus rings to extend */}
64
75
  <div className="-mx-2 px-2">{children}</div>
65
76
  </DialogPrimitive.Content>
66
- </DialogPortal>
67
- ));
77
+ </DialogPortal>
78
+ );
79
+ });
68
80
  DialogContent.displayName = DialogPrimitive.Content.displayName;
69
81
 
70
82
  const DialogHeader = ({
@@ -1,5 +1,6 @@
1
1
  import React, { useRef, useEffect } from "react";
2
2
  import { cn } from "../utils";
3
+ import { usePerformance } from "./PerformanceProvider";
3
4
 
4
5
  export const DropdownMenu: React.FC<{ children: React.ReactNode }> = ({
5
6
  children,
@@ -25,6 +26,7 @@ export const DropdownMenuContent: React.FC<{
25
26
  onClose: () => void;
26
27
  className?: string;
27
28
  }> = ({ children, isOpen, onClose, className }) => {
29
+ const { isLowPower } = usePerformance();
28
30
  const contentRef = useRef<HTMLDivElement>(null);
29
31
 
30
32
  useEffect(() => {
@@ -51,7 +53,8 @@ export const DropdownMenuContent: React.FC<{
51
53
  <div
52
54
  ref={contentRef}
53
55
  className={cn(
54
- "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100] animate-in fade-in zoom-in-95 duration-100",
56
+ "absolute right-0 mt-2 w-56 origin-top-right rounded-md bg-popover shadow-lg ring-1 ring-border focus:outline-none z-[100]",
57
+ !isLowPower && "animate-in fade-in zoom-in-95 duration-100",
55
58
  className
56
59
  )}
57
60
  >
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { Loader2, ChevronDown } from "lucide-react";
3
+ import { cn } from "../../utils";
3
4
 
4
5
  import {
5
6
  Input,
@@ -9,6 +10,7 @@ import {
9
10
  SelectItem,
10
11
  SelectTrigger,
11
12
  SelectValue,
13
+ usePerformance,
12
14
  } from "../../index";
13
15
 
14
16
  import type { DynamicOptionsFieldProps, ResolverOption } from "./types";
@@ -33,6 +35,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
33
35
  optionsResolvers,
34
36
  onChange,
35
37
  }) => {
38
+ const { isLowPower } = usePerformance();
36
39
  const [options, setOptions] = React.useState<ResolverOption[]>([]);
37
40
  const [loading, setLoading] = React.useState(true);
38
41
  const [error, setError] = React.useState<string | undefined>();
@@ -72,9 +75,7 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
72
75
  })
73
76
  .catch((error_) => {
74
77
  if (!cancelled) {
75
- setError(
76
- extractErrorMessage(error_, "Failed to load options"),
77
- );
78
+ setError(extractErrorMessage(error_, "Failed to load options"));
78
79
  setLoading(false);
79
80
  }
80
81
  });
@@ -199,7 +200,12 @@ export const DynamicOptionsField: React.FC<DynamicOptionsFieldProps> = ({
199
200
  <div className="relative">
200
201
  {loading ? (
201
202
  <div className="flex items-center gap-2 h-10 px-3 border rounded-md bg-muted/50">
202
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
203
+ <Loader2
204
+ className={cn(
205
+ "h-4 w-4 text-muted-foreground",
206
+ !isLowPower && "animate-spin",
207
+ )}
208
+ />
203
209
  <span className="text-sm text-muted-foreground">
204
210
  Loading options...
205
211
  </span>
@@ -1,5 +1,6 @@
1
1
  import React from "react";
2
2
  import { cn } from "../utils";
3
+ import { usePerformance } from "./PerformanceProvider";
3
4
 
4
5
  interface LoadingSpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
5
6
  size?: "sm" | "md" | "lg";
@@ -10,6 +11,8 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
10
11
  className,
11
12
  ...props
12
13
  }) => {
14
+ const { isLowPower } = usePerformance();
15
+
13
16
  const sizeClasses = {
14
17
  sm: "w-4 h-4 border-2",
15
18
  md: "w-8 h-8 border-4",
@@ -20,7 +23,8 @@ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
20
23
  <div className={cn("flex justify-center py-12", className)} {...props}>
21
24
  <div
22
25
  className={cn(
23
- "border-indigo-200 border-t-indigo-500 rounded-full animate-spin",
26
+ "border-indigo-200 border-t-indigo-500 rounded-full",
27
+ !isLowPower && "animate-spin",
24
28
  sizeClasses[size]
25
29
  )}
26
30
  />
@@ -4,6 +4,7 @@ import { ChevronDown } from "lucide-react";
4
4
  import { cn } from "../utils";
5
5
  import { useApi, accessApiRef } from "@checkstack/frontend-api";
6
6
  import type { AccessRule } from "@checkstack/common";
7
+ import { usePerformance } from "./PerformanceProvider";
7
8
 
8
9
  export interface NavItemProps {
9
10
  to?: string;
@@ -25,6 +26,7 @@ export const NavItem: React.FC<NavItemProps> = ({
25
26
  }) => {
26
27
  const [isOpen, setIsOpen] = useState(false);
27
28
  const containerRef = useRef<HTMLDivElement>(null);
29
+ const { isLowPower } = usePerformance();
28
30
 
29
31
  // Always call hooks at top level
30
32
  const accessApi = useApi(accessApiRef);
@@ -66,10 +68,6 @@ export const NavItem: React.FC<NavItemProps> = ({
66
68
 
67
69
  // Dropdown / Parent Item
68
70
  if (children) {
69
- // Check if any child is active to highlight parent
70
- // This is naive; normally we'd check paths.
71
- // For now, let's just rely on click state or strict path matching if 'to' is present on parent.
72
-
73
71
  return (
74
72
  <div className="relative group" ref={containerRef}>
75
73
  <button
@@ -88,7 +86,10 @@ export const NavItem: React.FC<NavItemProps> = ({
88
86
  </button>
89
87
 
90
88
  {isOpen && (
91
- <div className="absolute left-0 mt-1 w-48 rounded-md shadow-lg bg-popover ring-1 ring-border z-50 animate-in fade-in zoom-in-95 duration-100">
89
+ <div className={cn(
90
+ "absolute left-0 mt-1 w-48 rounded-md shadow-lg bg-popover ring-1 ring-border z-50",
91
+ !isLowPower && "animate-in fade-in zoom-in-95 duration-100"
92
+ )}>
92
93
  <div className="py-1 flex flex-col p-1 gap-1">{children}</div>
93
94
  </div>
94
95
  )}
@@ -0,0 +1,125 @@
1
+ import React, { createContext, useContext, useEffect, useState } from "react";
2
+
3
+ interface PerformanceContextValue {
4
+ /**
5
+ * Whether the device is considered low-power or lacks hardware acceleration.
6
+ * If true, expensive animations, blurs, and transitions should be disabled.
7
+ */
8
+ isLowPower: boolean;
9
+ /** Whether the performance detection has completed */
10
+ isLoaded: boolean;
11
+ }
12
+
13
+ const PerformanceContext = createContext<PerformanceContextValue>({
14
+ isLowPower: false,
15
+ isLoaded: false,
16
+ });
17
+
18
+ /**
19
+ * usePerformance - Hook to access the global hardware performance state.
20
+ * Use this to conditionally disable heavy visual effects.
21
+ */
22
+ export const usePerformance = () => useContext(PerformanceContext);
23
+
24
+ interface PerformanceProviderProps {
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ /**
29
+ * PerformanceProvider - Centralizes detection of hardware capabilities and user preferences.
30
+ * Runs a suite of heuristics (Media Queries, WebGL Audit, and Canvas Benchmarks)
31
+ * once on mount and provides the result to the entire application.
32
+ */
33
+ export const PerformanceProvider: React.FC<PerformanceProviderProps> = ({ children }) => {
34
+ const [state, setState] = useState<PerformanceContextValue>({
35
+ isLowPower: false,
36
+ isLoaded: false,
37
+ });
38
+
39
+ useEffect(() => {
40
+ const runPerformanceChecks = () => {
41
+ // 1. Accessibility Override (Reduced Motion)
42
+ const prefersReducedMotion = globalThis.matchMedia(
43
+ "(prefers-reduced-motion: reduce)"
44
+ ).matches;
45
+
46
+ // 2. Hardware Hint Check (Low RAM or CPU Cores)
47
+ const nav = globalThis.navigator as Navigator & { deviceMemory?: number };
48
+ const isLowEndHardware =
49
+ (nav.deviceMemory !== undefined && nav.deviceMemory < 4) ||
50
+ nav.hardwareConcurrency <= 2;
51
+
52
+ // 3. Renderer Audit (Detecting Software Rasterizers)
53
+ let isSoftwareRenderer = false;
54
+ try {
55
+ const canvas = document.createElement("canvas");
56
+ const gl =
57
+ canvas.getContext("webgl") ||
58
+ canvas.getContext("experimental-webgl");
59
+
60
+ if (gl instanceof WebGLRenderingContext) {
61
+ const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
62
+ const renderer = (
63
+ debugInfo
64
+ ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
65
+ : gl.getParameter(gl.RENDERER)
66
+ ).toLowerCase();
67
+
68
+ isSoftwareRenderer = [
69
+ "software",
70
+ "swiftshader",
71
+ "llvmpipe",
72
+ "softpipe",
73
+ "swrast",
74
+ "osmesa",
75
+ "mesa off-screen",
76
+ "basic render",
77
+ "warp",
78
+ ].some((id) => renderer.includes(id));
79
+ } else {
80
+ // No WebGL support at all usually implies old/stripped browser
81
+ isSoftwareRenderer = true;
82
+ }
83
+ } catch {
84
+ isSoftwareRenderer = true;
85
+ }
86
+
87
+ // 4. Empirical Benchmark (Stress Test)
88
+ let isSlow = false;
89
+ try {
90
+ const benchCanvas = document.createElement("canvas");
91
+ benchCanvas.width = 100;
92
+ benchCanvas.height = 100;
93
+ const ctx = benchCanvas.getContext("2d");
94
+ if (ctx) {
95
+ const t0 = globalThis.performance.now();
96
+ ctx.filter = "blur(20px)";
97
+ for (let i = 0; i < 50; i++) {
98
+ ctx.fillRect(i, i, 5, 5);
99
+ }
100
+ // Force pipeline sync to measure actual drawing time
101
+ ctx.getImageData(0, 0, 1, 1);
102
+ const t1 = globalThis.performance.now();
103
+ isSlow = t1 - t0 > 4; // Threshold for CPU-based rasterization
104
+ }
105
+ } catch {
106
+ isSlow = true;
107
+ }
108
+
109
+ const isLowPowerVerdict = prefersReducedMotion || isLowEndHardware || isSoftwareRenderer || isSlow;
110
+
111
+ setState({
112
+ isLowPower: isLowPowerVerdict,
113
+ isLoaded: true,
114
+ });
115
+ };
116
+
117
+ runPerformanceChecks();
118
+ }, []);
119
+
120
+ return (
121
+ <PerformanceContext.Provider value={state}>
122
+ {children}
123
+ </PerformanceContext.Provider>
124
+ );
125
+ };
@@ -2,6 +2,7 @@ import * as React from "react";
2
2
  import * as SelectPrimitive from "@radix-ui/react-select";
3
3
  import { Check, ChevronDown, ChevronUp } from "lucide-react";
4
4
  import { cn } from "../utils";
5
+ import { usePerformance } from "./PerformanceProvider";
5
6
 
6
7
  const Select = SelectPrimitive.Root;
7
8
 
@@ -67,19 +68,22 @@ SelectScrollDownButton.displayName =
67
68
  const SelectContent = React.forwardRef<
68
69
  React.ElementRef<typeof SelectPrimitive.Content>,
69
70
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
70
- >(({ className, children, position = "popper", ...props }, ref) => (
71
- <SelectPrimitive.Portal>
72
- <SelectPrimitive.Content
73
- ref={ref}
74
- className={cn(
75
- "relative z-[100] max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
76
- position === "popper" &&
77
- "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
78
- className
79
- )}
80
- position={position}
81
- {...props}
82
- >
71
+ >(({ className, children, position = "popper", ...props }, ref) => {
72
+ const { isLowPower } = usePerformance();
73
+ return (
74
+ <SelectPrimitive.Portal>
75
+ <SelectPrimitive.Content
76
+ ref={ref}
77
+ className={cn(
78
+ "relative z-[100] max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md",
79
+ !isLowPower && "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
80
+ position === "popper" &&
81
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
82
+ className
83
+ )}
84
+ position={position}
85
+ {...props}
86
+ >
83
87
  <SelectScrollUpButton />
84
88
  <SelectPrimitive.Viewport
85
89
  className={cn(
@@ -92,8 +96,9 @@ const SelectContent = React.forwardRef<
92
96
  </SelectPrimitive.Viewport>
93
97
  <SelectScrollDownButton />
94
98
  </SelectPrimitive.Content>
95
- </SelectPrimitive.Portal>
96
- ));
99
+ </SelectPrimitive.Portal>
100
+ );
101
+ });
97
102
  SelectContent.displayName = SelectPrimitive.Content.displayName;
98
103
 
99
104
  const SelectLabel = React.forwardRef<
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
2
2
  import { Button } from "./Button";
3
3
  import { Bell } from "lucide-react";
4
4
  import { cn } from "../utils";
5
+ import { usePerformance } from "./PerformanceProvider";
5
6
 
6
7
  export interface SubscribeButtonProps {
7
8
  /**
@@ -43,15 +44,16 @@ export const SubscribeButton: React.FC<SubscribeButtonProps> = ({
43
44
  className,
44
45
  }) => {
45
46
  const [animating, setAnimating] = useState(false);
47
+ const { isLowPower } = usePerformance();
46
48
 
47
49
  // Trigger animation when subscription state changes
48
50
  useEffect(() => {
49
- if (!loading) {
51
+ if (!loading && !isLowPower) {
50
52
  setAnimating(true);
51
53
  const timer = setTimeout(() => setAnimating(false), 500);
52
54
  return () => clearTimeout(timer);
53
55
  }
54
- }, [isSubscribed, loading]);
56
+ }, [isSubscribed, loading, isLowPower]);
55
57
 
56
58
  const handleClick = () => {
57
59
  if (loading) return;
@@ -94,7 +96,10 @@ export const SubscribeButton: React.FC<SubscribeButtonProps> = ({
94
96
  <span
95
97
  role="status"
96
98
  aria-label="Loading"
97
- className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
99
+ className={cn(
100
+ "h-4 w-4 rounded-full border-2 border-current border-t-transparent",
101
+ !isLowPower && "animate-spin"
102
+ )}
98
103
  />
99
104
  ) : (
100
105
  <Bell
@@ -102,7 +107,7 @@ export const SubscribeButton: React.FC<SubscribeButtonProps> = ({
102
107
  className={cn(
103
108
  "h-4 w-4 transition-all duration-300",
104
109
  isSubscribed && "fill-current",
105
- animating && "animate-bell-ring"
110
+ animating && !isLowPower && "animate-bell-ring"
106
111
  )}
107
112
  />
108
113
  )}
@@ -1,5 +1,6 @@
1
1
  import React, { useRef, useState, useEffect } from "react";
2
2
  import { cn } from "../utils";
3
+ import { usePerformance } from "./PerformanceProvider";
3
4
 
4
5
  export interface TerminalEntry {
5
6
  id: string;
@@ -65,6 +66,7 @@ export const TerminalFeed: React.FC<TerminalFeedProps> = ({
65
66
  title = "terminal",
66
67
  className,
67
68
  }) => {
69
+ const { isLowPower } = usePerformance();
68
70
  const contentRef = useRef<HTMLDivElement>(null);
69
71
  const [isHovering, setIsHovering] = useState(false);
70
72
 
@@ -107,7 +109,7 @@ export const TerminalFeed: React.FC<TerminalFeedProps> = ({
107
109
  onMouseLeave={() => setIsHovering(false)}
108
110
  >
109
111
  {displayEntries.length === 0 ? (
110
- <div className="text-gray-500 animate-pulse">
112
+ <div className={cn("text-gray-500", !isLowPower && "animate-pulse")}>
111
113
  Waiting for events...
112
114
  </div>
113
115
  ) : (
@@ -141,10 +143,13 @@ export const TerminalFeed: React.FC<TerminalFeedProps> = ({
141
143
  })
142
144
  )}
143
145
 
144
- {/* Blinking cursor */}
146
+ {/* Blinking cursor - animation disabled in low power mode */}
145
147
  <div className="flex items-center gap-1 mt-2">
146
148
  <span className="text-emerald-400">$</span>
147
- <span className="w-2 h-4 bg-emerald-400 animate-pulse" />
149
+ <span className={cn(
150
+ "w-2 h-4 bg-emerald-400",
151
+ !isLowPower && "animate-pulse"
152
+ )} />
148
153
  </div>
149
154
  </div>
150
155
  </div>
@@ -2,6 +2,7 @@ import React, { useEffect } from "react";
2
2
  import { cva, type VariantProps } from "class-variance-authority";
3
3
  import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from "lucide-react";
4
4
  import { cn } from "../utils";
5
+ import { usePerformance } from "./PerformanceProvider";
5
6
 
6
7
  const toastVariants = cva(
7
8
  "relative flex items-start gap-3 w-full max-w-md rounded-lg p-4 transition-all",
@@ -55,6 +56,7 @@ export const Toast: React.FC<ToastProps> = ({
55
56
  duration = 4000,
56
57
  onDismiss,
57
58
  }) => {
59
+ const { isLowPower } = usePerformance();
58
60
  const Icon = iconMap[variant || "default"];
59
61
  const iconColor = iconColorMap[variant || "default"];
60
62
  const [isHovered, setIsHovered] = React.useState(false);
@@ -97,7 +99,7 @@ export const Toast: React.FC<ToastProps> = ({
97
99
  <div
98
100
  className={cn(
99
101
  toastVariants({ variant }),
100
- "animate-in slide-in-from-right fade-in zoom-in-95 duration-300 hover:-translate-y-1 hover:shadow-2xl cursor-default"
102
+ !isLowPower && "animate-in slide-in-from-right fade-in zoom-in-95 duration-300 hover:-translate-y-1 hover:shadow-2xl cursor-default"
101
103
  )}
102
104
  role="alert"
103
105
  aria-live="polite"
package/src/index.ts CHANGED
@@ -49,6 +49,7 @@ export * from "./components/ColorPicker";
49
49
  export * from "./components/AnimatedCounter";
50
50
  export * from "./components/CommandPalette";
51
51
  export * from "./components/TerminalFeed";
52
+ export * from "./components/PerformanceProvider";
52
53
  export * from "./components/AmbientBackground";
53
54
  export * from "./components/CodeEditor";
54
55
  export * from "./components/AnimatedNumber";
package/src/themes.css CHANGED
@@ -143,41 +143,15 @@
143
143
 
144
144
  /* Custom animations */
145
145
  @keyframes bell-ring {
146
- 0% {
147
- transform: rotate(0deg);
148
- }
149
-
150
- 10% {
151
- transform: rotate(15deg);
152
- }
153
-
154
- 20% {
155
- transform: rotate(-15deg);
156
- }
157
-
158
- 30% {
159
- transform: rotate(10deg);
160
- }
161
-
162
- 40% {
163
- transform: rotate(-10deg);
164
- }
165
-
166
- 50% {
167
- transform: rotate(5deg);
168
- }
169
-
170
- 60% {
171
- transform: rotate(-5deg);
172
- }
173
-
174
- 70% {
175
- transform: rotate(0deg);
176
- }
177
-
178
- 100% {
179
- transform: rotate(0deg);
180
- }
146
+ 0% { transform: rotate(0deg); }
147
+ 10% { transform: rotate(15deg); }
148
+ 20% { transform: rotate(-15deg); }
149
+ 30% { transform: rotate(10deg); }
150
+ 40% { transform: rotate(-10deg); }
151
+ 50% { transform: rotate(5deg); }
152
+ 60% { transform: rotate(-5deg); }
153
+ 70% { transform: rotate(0deg); }
154
+ 100% { transform: rotate(0deg); }
181
155
  }
182
156
 
183
157
  .animate-bell-ring {
@@ -185,20 +159,48 @@
185
159
  transform-origin: top center;
186
160
  }
187
161
 
188
- /* Subtle tile pulse animation - gentle breathing effect */
189
- @keyframes tile-pulse {
162
+ /* High-performance ambient background styles */
163
+ @keyframes aurora-float-1 {
164
+ 0% { transform: translate(-15vw, -10vh) rotate(0deg); }
165
+ 50% { transform: translate(35vw, 25vh) rotate(180deg); }
166
+ 100% { transform: translate(-15vw, -10vh) rotate(360deg); }
167
+ }
190
168
 
191
- 0%,
192
- 100% {
193
- opacity: 1;
194
- }
169
+ @keyframes aurora-float-2 {
170
+ 0% { transform: translate(0, 0) scale(1); }
171
+ 50% { transform: translate(-30vw, 25vh) scale(1.2); }
172
+ 100% { transform: translate(0, 0) scale(1); }
173
+ }
195
174
 
196
- 50% {
197
- opacity: 0.4;
198
- }
175
+ @keyframes aurora-float-3 {
176
+ 0% { transform: translate(0, 0) rotate(0deg); }
177
+ 50% { transform: translate(25vw, -30vh) rotate(-180deg); }
178
+ 100% { transform: translate(0, 0) rotate(-360deg); }
179
+ }
180
+
181
+ @keyframes aurora-float-4 {
182
+ 0% { transform: translate(0, 0) scale(1.1); }
183
+ 50% { transform: translate(-20vw, -20vh) scale(1); }
184
+ 100% { transform: translate(0, 0) scale(1.1); }
185
+ }
186
+
187
+ .ambient-grid {
188
+ background-image:
189
+ linear-gradient(to right, hsl(var(--muted-foreground) / 0.15) 1px, transparent 1px),
190
+ linear-gradient(to bottom, hsl(var(--muted-foreground) / 0.15) 1px, transparent 1px);
191
+ background-size: 48px 48px;
192
+ }
193
+
194
+ .ambient-grid-inverse {
195
+ background: hsl(var(--background));
196
+ -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='48' height='48' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='47' height='47' fill='black'/%3E%3C/svg%3E");
197
+ mask-image: url("data:image/svg+xml,%3Csvg width='48' height='48' xmlns='http://www.w3.org/2000/svg'%3E%3Crect x='1' y='1' width='47' height='47' fill='black'/%3E%3C/svg%3E");
199
198
  }
200
199
 
201
- .tile-glow {
202
- animation: tile-pulse 12s ease-in-out infinite;
203
- animation-delay: var(--glow-delay, 0s);
200
+ .aurora-blob {
201
+ position: absolute;
202
+ filter: blur(60px);
203
+ opacity: 0.6;
204
+ pointer-events: none;
205
+ will-change: transform;
204
206
  }