@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 +12 -0
- package/package.json +1 -1
- package/src/components/AmbientBackground.tsx +61 -70
- package/src/components/AnimatedCounter.tsx +7 -4
- package/src/components/CommandPalette.tsx +9 -4
- package/src/components/Dialog.tsx +33 -21
- package/src/components/DropdownMenu.tsx +4 -1
- package/src/components/DynamicForm/DynamicOptionsField.tsx +10 -4
- package/src/components/LoadingSpinner.tsx +5 -1
- package/src/components/NavItem.tsx +6 -5
- package/src/components/PerformanceProvider.tsx +125 -0
- package/src/components/Select.tsx +20 -15
- package/src/components/SubscribeButton.tsx +9 -4
- package/src/components/TerminalFeed.tsx +8 -3
- package/src/components/Toast.tsx +3 -1
- package/src/index.ts +1 -0
- package/src/themes.css +49 -47
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,104 +1,95 @@
|
|
|
1
|
-
import React, { useMemo
|
|
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 -
|
|
13
|
-
* Features
|
|
14
|
-
*
|
|
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
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
96
|
-
linear-gradient(to bottom, hsl(var(--background)) 0%, transparent
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
className
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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
|
|
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=
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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>
|
package/src/components/Toast.tsx
CHANGED
|
@@ -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
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
/*
|
|
189
|
-
@keyframes
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
.
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
.aurora-blob {
|
|
201
|
+
position: absolute;
|
|
202
|
+
filter: blur(60px);
|
|
203
|
+
opacity: 0.6;
|
|
204
|
+
pointer-events: none;
|
|
205
|
+
will-change: transform;
|
|
204
206
|
}
|