@checkstack/ui 1.0.0 → 1.1.1

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,41 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - a340781: Improve accessibility of SubscribeButton component by adding appropriate ARIA labels and attributes.
8
+ - 8d2660d: Added `@testing-library/react` to devDependencies.
9
+ - Updated dependencies [0ebbe56]
10
+ - @checkstack/common@0.6.3
11
+ - @checkstack/frontend-api@0.3.6
12
+
13
+ ## 1.1.0
14
+
15
+ ### Minor Changes
16
+
17
+ - c842373: ## Animated Numbers & Availability Stats Live Updates
18
+
19
+ ### Features
20
+
21
+ - **AnimatedNumber component** (`@checkstack/ui`): New reusable component that displays numbers with a smooth "rolling" animation when values change. Uses `requestAnimationFrame` with eased interpolation for a polished effect.
22
+ - **useAnimatedNumber hook** (`@checkstack/ui`): Underlying hook for the animation logic, can be used directly for custom implementations.
23
+ - **Live availability updates**: Availability stats (31-day and 365-day) now automatically refresh when new health check runs are received via signals.
24
+
25
+ ### Usage
26
+
27
+ ```tsx
28
+ import { AnimatedNumber } from "@checkstack/ui";
29
+
30
+ <AnimatedNumber
31
+ value={99.95}
32
+ suffix="%"
33
+ decimals={2}
34
+ duration={500}
35
+ className="text-2xl font-bold text-green-500"
36
+ />;
37
+ ```
38
+
3
39
  ## 1.0.0
4
40
 
5
41
  ### Major Changes
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
7
- "@checkstack/common": "0.6.1",
8
- "@checkstack/frontend-api": "0.3.4",
7
+ "@checkstack/common": "0.6.2",
8
+ "@checkstack/frontend-api": "0.3.5",
9
9
  "@monaco-editor/react": "^4.7.0",
10
10
  "@radix-ui/react-accordion": "^1.2.12",
11
11
  "@radix-ui/react-dialog": "^1.1.15",
@@ -29,6 +29,7 @@
29
29
  "devDependencies": {
30
30
  "typescript": "^5.0.0",
31
31
  "@types/react": "^18.2.0",
32
+ "@testing-library/react": "^16.0.0",
32
33
  "@checkstack/test-utils-frontend": "0.0.3",
33
34
  "@checkstack/tsconfig": "0.0.3",
34
35
  "@checkstack/scripts": "0.1.1"
@@ -0,0 +1,48 @@
1
+ import { useAnimatedNumber } from "../hooks/useAnimatedNumber";
2
+
3
+ interface AnimatedNumberProps {
4
+ /** The number value to display (undefined for N/A) */
5
+ value: number | undefined;
6
+ /** Animation duration in milliseconds (default: 500ms) */
7
+ duration?: number;
8
+ /** Number of decimal places (default: 2) */
9
+ decimals?: number;
10
+ /** Suffix to append after the number (e.g., "%", "ms") */
11
+ suffix?: string;
12
+ /** CSS classes for the number span */
13
+ className?: string;
14
+ /** CSS classes for the suffix span */
15
+ suffixClassName?: string;
16
+ }
17
+
18
+ /**
19
+ * Component that displays an animated number with smooth rolling effect.
20
+ * Numbers smoothly interpolate from their previous value to the new value.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <AnimatedNumber
25
+ * value={99.95}
26
+ * suffix="%"
27
+ * className="text-2xl font-bold text-green-500"
28
+ * />
29
+ * ```
30
+ */
31
+ export function AnimatedNumber({
32
+ value,
33
+ duration = 500,
34
+ decimals = 2,
35
+ suffix,
36
+ className = "",
37
+ suffixClassName = "",
38
+ }: AnimatedNumberProps) {
39
+ const displayValue = useAnimatedNumber(value, duration, decimals);
40
+ const isNA = value === undefined;
41
+
42
+ return (
43
+ <span className={`tabular-nums ${className}`}>
44
+ {displayValue}
45
+ {!isNA && suffix && <span className={suffixClassName}>{suffix}</span>}
46
+ </span>
47
+ );
48
+ }
@@ -79,11 +79,26 @@ export const SubscribeButton: React.FC<SubscribeButtonProps> = ({
79
79
  ? "Unsubscribe from notifications"
80
80
  : "Subscribe to notifications"
81
81
  }
82
+ aria-label={
83
+ loading
84
+ ? isSubscribed
85
+ ? "Unsubscribing..."
86
+ : "Subscribing..."
87
+ : isSubscribed
88
+ ? "Unsubscribe from notifications"
89
+ : "Subscribe to notifications"
90
+ }
91
+ aria-pressed={isSubscribed}
82
92
  >
83
93
  {loading ? (
84
- <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" />
94
+ <span
95
+ role="status"
96
+ aria-label="Loading"
97
+ className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
98
+ />
85
99
  ) : (
86
100
  <Bell
101
+ aria-hidden="true"
87
102
  className={cn(
88
103
  "h-4 w-4 transition-all duration-300",
89
104
  isSubscribed && "fill-current",
@@ -0,0 +1,83 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+
3
+ /**
4
+ * Hook that animates a number from its previous value to the new value.
5
+ * Creates a smooth "rolling numbers" effect.
6
+ *
7
+ * @param targetValue - The value to animate towards (undefined for N/A)
8
+ * @param duration - Animation duration in milliseconds (default: 500ms)
9
+ * @param decimals - Number of decimal places to display (default: 2)
10
+ */
11
+ export function useAnimatedNumber(
12
+ targetValue: number | undefined,
13
+ duration = 500,
14
+ decimals = 2,
15
+ ): string {
16
+ const [displayValue, setDisplayValue] = useState(targetValue);
17
+ const animationRef = useRef<number>();
18
+ const startTimeRef = useRef<number>();
19
+ const startValueRef = useRef<number | undefined>(undefined);
20
+
21
+ useEffect(() => {
22
+ if (targetValue === undefined) {
23
+ setDisplayValue(undefined);
24
+ return;
25
+ }
26
+
27
+ // If this is the first value, just set it immediately
28
+ if (startValueRef.current === undefined) {
29
+ startValueRef.current = targetValue;
30
+ setDisplayValue(targetValue);
31
+ return;
32
+ }
33
+
34
+ const startValue = startValueRef.current;
35
+
36
+ // If target is the same, no animation needed
37
+ if (startValue === targetValue) {
38
+ return;
39
+ }
40
+
41
+ // Cancel any existing animation
42
+ if (animationRef.current) {
43
+ cancelAnimationFrame(animationRef.current);
44
+ }
45
+
46
+ const animate = (timestamp: number) => {
47
+ if (!startTimeRef.current) {
48
+ startTimeRef.current = timestamp;
49
+ }
50
+
51
+ const elapsed = timestamp - startTimeRef.current;
52
+ const progress = Math.min(elapsed / duration, 1);
53
+
54
+ // Ease out cubic for smooth deceleration
55
+ const eased = 1 - Math.pow(1 - progress, 3);
56
+
57
+ const current = startValue + (targetValue - startValue) * eased;
58
+ setDisplayValue(current);
59
+
60
+ if (progress < 1) {
61
+ animationRef.current = requestAnimationFrame(animate);
62
+ } else {
63
+ // Animation complete - update the start value for next animation
64
+ startValueRef.current = targetValue;
65
+ startTimeRef.current = undefined;
66
+ }
67
+ };
68
+
69
+ animationRef.current = requestAnimationFrame(animate);
70
+
71
+ return () => {
72
+ if (animationRef.current) {
73
+ cancelAnimationFrame(animationRef.current);
74
+ }
75
+ };
76
+ }, [targetValue, duration]);
77
+
78
+ if (displayValue === undefined) {
79
+ return "N/A";
80
+ }
81
+
82
+ return displayValue.toFixed(decimals);
83
+ }
package/src/index.ts CHANGED
@@ -51,3 +51,5 @@ export * from "./components/CommandPalette";
51
51
  export * from "./components/TerminalFeed";
52
52
  export * from "./components/AmbientBackground";
53
53
  export * from "./components/CodeEditor";
54
+ export * from "./components/AnimatedNumber";
55
+ export * from "./hooks/useAnimatedNumber";