@checkstack/ui 0.0.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.
Files changed (68) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +40 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +90 -0
  6. package/src/components/AmbientBackground.tsx +105 -0
  7. package/src/components/AnimatedCounter.tsx +54 -0
  8. package/src/components/BackLink.tsx +56 -0
  9. package/src/components/Badge.tsx +38 -0
  10. package/src/components/Button.tsx +55 -0
  11. package/src/components/Card.tsx +56 -0
  12. package/src/components/Checkbox.tsx +46 -0
  13. package/src/components/ColorPicker.tsx +69 -0
  14. package/src/components/CommandPalette.tsx +74 -0
  15. package/src/components/ConfirmationModal.tsx +134 -0
  16. package/src/components/DateRangeFilter.tsx +128 -0
  17. package/src/components/DateTimePicker.tsx +65 -0
  18. package/src/components/Dialog.tsx +134 -0
  19. package/src/components/DropdownMenu.tsx +96 -0
  20. package/src/components/DynamicForm/DynamicForm.tsx +126 -0
  21. package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
  22. package/src/components/DynamicForm/FormField.tsx +690 -0
  23. package/src/components/DynamicForm/JsonField.tsx +98 -0
  24. package/src/components/DynamicForm/index.ts +11 -0
  25. package/src/components/DynamicForm/types.ts +95 -0
  26. package/src/components/DynamicForm/utils.ts +39 -0
  27. package/src/components/DynamicIcon.tsx +45 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +57 -0
  31. package/src/components/InfoBanner.tsx +97 -0
  32. package/src/components/Input.tsx +20 -0
  33. package/src/components/Label.tsx +17 -0
  34. package/src/components/LoadingSpinner.tsx +29 -0
  35. package/src/components/Markdown.tsx +206 -0
  36. package/src/components/NavItem.tsx +112 -0
  37. package/src/components/Page.tsx +58 -0
  38. package/src/components/PageLayout.tsx +83 -0
  39. package/src/components/PaginatedList.tsx +135 -0
  40. package/src/components/Pagination.tsx +195 -0
  41. package/src/components/PermissionDenied.tsx +31 -0
  42. package/src/components/PermissionGate.tsx +97 -0
  43. package/src/components/PluginConfigForm.tsx +91 -0
  44. package/src/components/SectionHeader.tsx +30 -0
  45. package/src/components/Select.tsx +157 -0
  46. package/src/components/StatusCard.tsx +78 -0
  47. package/src/components/StatusUpdateTimeline.tsx +222 -0
  48. package/src/components/StrategyConfigCard.tsx +333 -0
  49. package/src/components/SubscribeButton.tsx +96 -0
  50. package/src/components/Table.tsx +119 -0
  51. package/src/components/Tabs.tsx +141 -0
  52. package/src/components/TemplateEditor.test.ts +156 -0
  53. package/src/components/TemplateEditor.tsx +435 -0
  54. package/src/components/TerminalFeed.tsx +152 -0
  55. package/src/components/Textarea.tsx +22 -0
  56. package/src/components/ThemeProvider.tsx +76 -0
  57. package/src/components/Toast.tsx +118 -0
  58. package/src/components/ToastProvider.tsx +126 -0
  59. package/src/components/Toggle.tsx +47 -0
  60. package/src/components/Tooltip.tsx +20 -0
  61. package/src/components/UserMenu.tsx +79 -0
  62. package/src/hooks/usePagination.e2e.ts +275 -0
  63. package/src/hooks/usePagination.ts +231 -0
  64. package/src/index.ts +53 -0
  65. package/src/themes.css +204 -0
  66. package/src/utils/strip-markdown.ts +44 -0
  67. package/src/utils.ts +8 -0
  68. package/tsconfig.json +6 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,153 @@
1
+ # @checkstack/ui
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - d20d274: Initial release of all @checkstack packages. Rebranded from Checkmate to Checkstack with new npm organization @checkstack and domain checkstack.dev.
8
+ - Updated dependencies [d20d274]
9
+ - @checkstack/common@0.0.2
10
+ - @checkstack/frontend-api@0.0.2
11
+
12
+ ## 0.1.2
13
+
14
+ ### Patch Changes
15
+
16
+ - 52231ef: # Auth Settings Page Refactoring
17
+
18
+ ## Auth Frontend
19
+
20
+ Refactored the `AuthSettingsPage` into modular, self-contained tab components:
21
+
22
+ - **New Components**: Created `UsersTab`, `RolesTab`, `StrategiesTab`, and `ApplicationsTab` components
23
+ - **Dynamic Tab Visibility**: Tabs are now conditionally shown based on user permissions
24
+ - **Auto-Select Logic**: Automatically selects the first available tab if the current tab becomes inaccessible
25
+ - **Self-Contained State**: Each tab component manages its own state, handlers, and dialogs, reducing prop drilling
26
+
27
+ ## UI Package
28
+
29
+ - **Responsive Tabs**: Tabs now use column layout on small screens and row layout on medium+ screens
30
+
31
+ - b0124ef: Fix light mode contrast for semantic color tokens
32
+
33
+ Updated the theme system to use a two-tier pattern for semantic colors:
34
+
35
+ - Base tokens (`text-destructive`, `text-success`, etc.) are used for text on light backgrounds (`bg-{color}/10`)
36
+ - Foreground tokens (`text-destructive-foreground`, etc.) are now white/contrasting and used for text on solid backgrounds
37
+
38
+ This fixes poor contrast issues with components like the "Incident" badge which had dark red text on a bright red background in light mode.
39
+
40
+ Components updated: Alert, InfoBanner, HealthBadge, Badge, PermissionDenied, SystemDetailPage
41
+
42
+ - 54cc787: ### Fix Access Denied Flash on Page Load
43
+
44
+ Fixed the "Access Denied" screen briefly flashing when loading permission-protected pages.
45
+
46
+ **Root cause:** The `usePermissions` hook was setting `loading: false` when the session was still pending, causing a brief moment where permissions appeared to be denied.
47
+
48
+ **Changes:**
49
+
50
+ - `usePermissions` hook now waits for session to finish loading (`isPending`) before determining permission state
51
+ - `PageLayout` component now treats `loading=undefined` with `allowed=false` as a loading state
52
+ - `AuthSettingsPage` now explicitly waits for permission hooks to finish loading before checking access
53
+
54
+ **Result:** Pages show a loading spinner until permissions are fully resolved, eliminating the flash.
55
+
56
+ - a65e002: Add compile-time type safety for Lucide icon names
57
+
58
+ - Add `LucideIconName` type and `lucideIconSchema` Zod schema to `@checkstack/common`
59
+ - Update backend interfaces (`AuthStrategy`, `NotificationStrategy`, `IntegrationProvider`, `CommandDefinition`) to use `LucideIconName`
60
+ - Update RPC contracts to use `lucideIconSchema` for proper type inference across RPC boundaries
61
+ - Simplify `SocialProviderButton` to use `DynamicIcon` directly (removes 30+ lines of pascalCase conversion)
62
+ - Replace static `iconMap` in `SearchDialog` with `DynamicIcon` for dynamic icon rendering
63
+ - Add fallback handling in `DynamicIcon` when icon name isn't found
64
+ - Fix legacy kebab-case icon names to PascalCase: `mail`→`Mail`, `send`→`Send`, `github`→`Github`, `key-round`→`KeyRound`, `network`→`Network`, `AlertCircle`→`CircleAlert`
65
+
66
+ - Updated dependencies [a65e002]
67
+ - Updated dependencies [ae33df2]
68
+ - Updated dependencies [32ea706]
69
+ - @checkstack/common@0.2.0
70
+ - @checkstack/frontend-api@0.1.0
71
+
72
+ ## 0.1.1
73
+
74
+ ### Patch Changes
75
+
76
+ - Updated dependencies [0f8cc7d]
77
+ - @checkstack/frontend-api@0.0.3
78
+
79
+ ## 0.1.0
80
+
81
+ ### Minor Changes
82
+
83
+ - ffc28f6: ### Anonymous Role and Public Access
84
+
85
+ Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
86
+
87
+ **Core Changes:**
88
+
89
+ - Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
90
+ - Renamed `userType: "both"` to `"authenticated"` for clarity
91
+ - Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
92
+ - Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
93
+
94
+ **Backend Infrastructure:**
95
+
96
+ - New `anonymous` system role created during auth-backend initialization
97
+ - New `disabled_public_default_permission` table tracks admin-disabled public defaults
98
+ - `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
99
+ - `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
100
+ - Anonymous role filtered from `getRoles` endpoint (not assignable to users)
101
+ - Validation prevents assigning anonymous role to users
102
+
103
+ **Catalog Integration:**
104
+
105
+ - `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
106
+ - Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
107
+
108
+ **UI:**
109
+
110
+ - New `PermissionGate` component for conditionally rendering content based on permissions
111
+
112
+ - b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
113
+
114
+ ## Strategy Instructions Interface
115
+
116
+ Added `adminInstructions` and `userInstructions` optional fields to the `NotificationStrategy` interface. These allow strategies to export markdown-formatted setup guides that are displayed in the configuration UI:
117
+
118
+ - **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
119
+ - **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
120
+
121
+ ### Updated Components
122
+
123
+ - `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
124
+ - `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
125
+ - `UserChannelCard` renders `userInstructions` when users need to connect
126
+
127
+ ## New Telegram Notification Plugin
128
+
129
+ Added `@checkstack/notification-telegram-backend` plugin for sending notifications via Telegram:
130
+
131
+ - Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
132
+ - Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
133
+ - Includes comprehensive admin instructions for bot setup via @BotFather
134
+ - Includes user instructions for account linking
135
+
136
+ ### Configuration
137
+
138
+ Admins need to configure a Telegram Bot Token obtained from @BotFather.
139
+
140
+ ### User Linking
141
+
142
+ The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
143
+
144
+ ### Patch Changes
145
+
146
+ - eff5b4e: Add standalone maintenance scheduling plugin
147
+
148
+ - New `@checkstack/maintenance-common` package with Zod schemas, permissions, oRPC contract, and extension slots
149
+ - New `@checkstack/maintenance-backend` package with Drizzle schema, service, and oRPC router
150
+ - New `@checkstack/maintenance-frontend` package with admin page and system detail panel
151
+ - Shared `DateTimePicker` component added to `@checkstack/ui`
152
+ - Database migrations for maintenances, maintenance_systems, and maintenance_updates tables
153
+ - @checkstack/frontend-api@0.0.2
package/bunfig.toml ADDED
@@ -0,0 +1,2 @@
1
+ [test]
2
+ preload = ["@checkstack/test-utils-frontend/setup"]
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@checkstack/ui",
3
+ "version": "0.0.2",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "dependencies": {
7
+ "@checkstack/common": "workspace:*",
8
+ "@checkstack/frontend-api": "workspace:*",
9
+ "@radix-ui/react-accordion": "^1.2.12",
10
+ "@radix-ui/react-dialog": "^1.1.15",
11
+ "@radix-ui/react-select": "^2.2.6",
12
+ "@radix-ui/react-slot": "^1.2.4",
13
+ "ajv": "^8.17.1",
14
+ "ajv-formats": "^3.0.1",
15
+ "class-variance-authority": "^0.7.1",
16
+ "clsx": "^2.1.0",
17
+ "date-fns": "^4.1.0",
18
+ "lucide-react": "0.562.0",
19
+ "prismjs": "^1.29.0",
20
+ "react": "^18.2.0",
21
+ "react-markdown": "^10.1.0",
22
+ "react-router-dom": "^6.20.0",
23
+ "react-simple-code-editor": "^0.14.1",
24
+ "recharts": "^3.6.0",
25
+ "tailwind-merge": "^2.2.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.0.0",
29
+ "@types/react": "^18.2.0",
30
+ "@checkstack/test-utils-frontend": "workspace:*",
31
+ "@checkstack/tsconfig": "workspace:*",
32
+ "@checkstack/scripts": "workspace:*"
33
+ },
34
+ "scripts": {
35
+ "typecheck": "tsc --noEmit",
36
+ "lint": "bun run lint:code",
37
+ "lint:code": "eslint . --max-warnings 0",
38
+ "test": "bun test"
39
+ }
40
+ }
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
3
+ import { ChevronDown } from "lucide-react";
4
+ import { cn } from "../utils";
5
+
6
+ const Accordion = AccordionPrimitive.Root;
7
+
8
+ const AccordionItem = React.forwardRef<
9
+ React.ElementRef<typeof AccordionPrimitive.Item>,
10
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
11
+ >(({ className, ...props }, ref) => (
12
+ <AccordionPrimitive.Item
13
+ ref={ref}
14
+ className={cn("border-b", className)}
15
+ {...props}
16
+ />
17
+ ));
18
+ AccordionItem.displayName = "AccordionItem";
19
+
20
+ const AccordionTrigger = React.forwardRef<
21
+ React.ElementRef<typeof AccordionPrimitive.Trigger>,
22
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
23
+ >(({ className, children, ...props }, ref) => (
24
+ <AccordionPrimitive.Header className="flex">
25
+ <AccordionPrimitive.Trigger
26
+ ref={ref}
27
+ className={cn(
28
+ "flex flex-1 items-center justify-between py-4 font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
29
+ className
30
+ )}
31
+ {...props}
32
+ >
33
+ {children}
34
+ <ChevronDown className="h-4 w-4 shrink-0 transition-transform duration-200" />
35
+ </AccordionPrimitive.Trigger>
36
+ </AccordionPrimitive.Header>
37
+ ));
38
+ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
39
+
40
+ const AccordionContent = React.forwardRef<
41
+ React.ElementRef<typeof AccordionPrimitive.Content>,
42
+ React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
43
+ >(({ className, children, ...props }, ref) => (
44
+ <AccordionPrimitive.Content
45
+ ref={ref}
46
+ className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
47
+ {...props}
48
+ >
49
+ <div className={cn("pb-4 pt-0", className)}>{children}</div>
50
+ </AccordionPrimitive.Content>
51
+ ));
52
+
53
+ AccordionContent.displayName = AccordionPrimitive.Content.displayName;
54
+
55
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
@@ -0,0 +1,90 @@
1
+ import React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ const alertVariants = cva("relative w-full rounded-md border p-4", {
5
+ variants: {
6
+ variant: {
7
+ default: "bg-muted/50 border-border text-foreground",
8
+ success: "bg-success/10 border-success/30 text-success",
9
+ warning: "bg-warning/10 border-warning/30 text-warning",
10
+ error: "bg-destructive/10 border-destructive/30 text-destructive",
11
+ info: "bg-info/10 border-info/30 text-info",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ variant: "default",
16
+ },
17
+ });
18
+
19
+ export interface AlertProps
20
+ extends React.HTMLAttributes<HTMLDivElement>,
21
+ VariantProps<typeof alertVariants> {
22
+ children: React.ReactNode;
23
+ }
24
+
25
+ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
26
+ ({ className, variant, children, ...props }, ref) => {
27
+ return (
28
+ <div
29
+ ref={ref}
30
+ role="alert"
31
+ className={alertVariants({ variant, className })}
32
+ {...props}
33
+ >
34
+ <div className="flex gap-3 items-center">{children}</div>
35
+ </div>
36
+ );
37
+ }
38
+ );
39
+
40
+ Alert.displayName = "Alert";
41
+
42
+ export const AlertIcon = React.forwardRef<
43
+ HTMLDivElement,
44
+ React.HTMLAttributes<HTMLDivElement>
45
+ >(({ className, children, ...props }, ref) => (
46
+ <div
47
+ ref={ref}
48
+ className={`flex-shrink-0 opacity-70 ${className || ""}`}
49
+ {...props}
50
+ >
51
+ {children}
52
+ </div>
53
+ ));
54
+
55
+ AlertIcon.displayName = "AlertIcon";
56
+
57
+ export const AlertContent = React.forwardRef<
58
+ HTMLDivElement,
59
+ React.HTMLAttributes<HTMLDivElement>
60
+ >(({ className, ...props }, ref) => (
61
+ <div ref={ref} className={`flex-1 space-y-1 ${className || ""}`} {...props} />
62
+ ));
63
+
64
+ AlertContent.displayName = "AlertContent";
65
+
66
+ export const AlertTitle = React.forwardRef<
67
+ HTMLHeadingElement,
68
+ React.HTMLAttributes<HTMLHeadingElement>
69
+ >(({ className, ...props }, ref) => (
70
+ <h5
71
+ ref={ref}
72
+ className={`font-semibold text-sm leading-tight ${className || ""}`}
73
+ {...props}
74
+ />
75
+ ));
76
+
77
+ AlertTitle.displayName = "AlertTitle";
78
+
79
+ export const AlertDescription = React.forwardRef<
80
+ HTMLParagraphElement,
81
+ React.HTMLAttributes<HTMLParagraphElement>
82
+ >(({ className, ...props }, ref) => (
83
+ <div
84
+ ref={ref}
85
+ className={`text-sm leading-relaxed opacity-90 ${className || ""}`}
86
+ {...props}
87
+ />
88
+ ));
89
+
90
+ AlertDescription.displayName = "AlertDescription";
@@ -0,0 +1,105 @@
1
+ import React, { useMemo, useEffect, useState } from "react";
2
+ import { cn } from "../utils";
3
+
4
+ interface AmbientBackgroundProps {
5
+ children: React.ReactNode;
6
+ className?: string;
7
+ }
8
+
9
+ const TILE_SIZE = 48;
10
+
11
+ /**
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.
15
+ */
16
+ export const AmbientBackground: React.FC<AmbientBackgroundProps> = ({
17
+ children,
18
+ className,
19
+ }) => {
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 }> = [];
39
+
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]);
55
+
56
+ return (
57
+ <div
58
+ className={cn(
59
+ "relative min-h-screen bg-background overflow-hidden",
60
+ className
61
+ )}
62
+ >
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
+ ))}
88
+ </div>
89
+
90
+ {/* Edge vignette to fade out the grid smoothly */}
91
+ <div
92
+ className="pointer-events-none fixed inset-0"
93
+ style={{
94
+ 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%)
97
+ `,
98
+ }}
99
+ />
100
+
101
+ {/* Content */}
102
+ <div className="relative z-10">{children}</div>
103
+ </div>
104
+ );
105
+ };
@@ -0,0 +1,54 @@
1
+ import React, { useEffect, useState, useRef } from "react";
2
+
3
+ interface AnimatedCounterProps {
4
+ value: number;
5
+ duration?: number;
6
+ formatter?: (n: number) => string;
7
+ className?: string;
8
+ }
9
+
10
+ /**
11
+ * AnimatedCounter - Animates a number from 0 to target value
12
+ * Uses requestAnimationFrame for smooth 60fps animation
13
+ */
14
+ export const AnimatedCounter: React.FC<AnimatedCounterProps> = ({
15
+ value,
16
+ duration = 500,
17
+ formatter = (n) => Math.round(n).toString(),
18
+ className,
19
+ }) => {
20
+ const [displayValue, setDisplayValue] = useState(0);
21
+ const displayValueRef = useRef(displayValue);
22
+ displayValueRef.current = displayValue;
23
+
24
+ useEffect(() => {
25
+ // Skip animation if value is 0 or duration is 0
26
+ if (value === 0 || duration === 0) {
27
+ setDisplayValue(value);
28
+ return;
29
+ }
30
+
31
+ const startTime = performance.now();
32
+ const startValue = displayValueRef.current;
33
+ const diff = value - startValue;
34
+
35
+ const animate = (currentTime: number) => {
36
+ const elapsed = currentTime - startTime;
37
+ const progress = Math.min(elapsed / duration, 1);
38
+
39
+ // Ease-out cubic for smooth deceleration
40
+ const easeOut = 1 - Math.pow(1 - progress, 3);
41
+ const current = startValue + diff * easeOut;
42
+
43
+ setDisplayValue(current);
44
+
45
+ if (progress < 1) {
46
+ requestAnimationFrame(animate);
47
+ }
48
+ };
49
+
50
+ requestAnimationFrame(animate);
51
+ }, [value, duration]);
52
+
53
+ return <span className={className}>{formatter(displayValue)}</span>;
54
+ };
@@ -0,0 +1,56 @@
1
+ import React from "react";
2
+ import { ArrowLeft } from "lucide-react";
3
+
4
+ interface BackLinkProps {
5
+ /** The destination to navigate to. Can be a path string or onClick handler. */
6
+ to?: string;
7
+ /** Click handler for navigation. If not provided with 'to', component will render a button. */
8
+ onClick?: () => void;
9
+ /** The text to display. Defaults to "Back" */
10
+ children?: React.ReactNode;
11
+ /** Optional className for custom styling */
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * A standardized back navigation link component.
17
+ * Use with react-router's Link by wrapping it in your own Link component
18
+ * or pass an onClick handler for programmatic navigation.
19
+ */
20
+ export const BackLink: React.FC<BackLinkProps> = ({
21
+ to,
22
+ onClick,
23
+ children = "Back",
24
+ className = "",
25
+ }) => {
26
+ const baseClasses =
27
+ "flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors";
28
+
29
+ // If 'to' is provided, render an anchor-like element that can be wrapped by Link
30
+ // Otherwise render a button with onClick
31
+ if (to) {
32
+ return (
33
+ <a
34
+ href={to}
35
+ onClick={(e) => {
36
+ // Let react-router handle actual navigation if this is wrapped in Link
37
+ if (onClick) {
38
+ e.preventDefault();
39
+ onClick();
40
+ }
41
+ }}
42
+ className={`${baseClasses} ${className}`}
43
+ >
44
+ <ArrowLeft className="h-4 w-4" />
45
+ {children}
46
+ </a>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <button onClick={onClick} className={`${baseClasses} ${className}`}>
52
+ <ArrowLeft className="h-4 w-4" />
53
+ {children}
54
+ </button>
55
+ );
56
+ };
@@ -0,0 +1,38 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+ import { cn } from "../utils";
4
+
5
+ const badgeVariants = cva(
6
+ "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default:
11
+ "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
12
+ secondary:
13
+ "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ destructive:
15
+ "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
16
+ outline: "text-foreground",
17
+ success:
18
+ "border-transparent bg-success/10 text-success hover:bg-success/20",
19
+ warning:
20
+ "border-transparent bg-warning/10 text-warning hover:bg-warning/20",
21
+ info: "border-transparent bg-info/10 text-info hover:bg-info/20",
22
+ },
23
+ },
24
+ defaultVariants: {
25
+ variant: "default",
26
+ },
27
+ }
28
+ );
29
+
30
+ export interface BadgeProps
31
+ extends React.HTMLAttributes<HTMLDivElement>,
32
+ VariantProps<typeof badgeVariants> {}
33
+
34
+ export function Badge({ className, variant, ...props }: BadgeProps) {
35
+ return (
36
+ <div className={cn(badgeVariants({ variant }), className)} {...props} />
37
+ );
38
+ }
@@ -0,0 +1,55 @@
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+ import { cn } from "../utils";
5
+
6
+ const buttonVariants = cva(
7
+ "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ primary: "bg-primary text-primary-foreground hover:bg-primary/90",
12
+ secondary:
13
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
14
+ outline:
15
+ "border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
16
+ ghost: "hover:bg-accent hover:text-accent-foreground",
17
+ destructive:
18
+ "bg-destructive text-destructive-foreground hover:bg-destructive/90",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default: "h-10 px-4 py-2",
23
+ sm: "h-9 rounded-md px-3",
24
+ lg: "h-11 rounded-md px-8",
25
+ icon: "h-10 w-10",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "primary",
30
+ size: "default",
31
+ },
32
+ }
33
+ );
34
+
35
+ export interface ButtonProps
36
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
37
+ VariantProps<typeof buttonVariants> {
38
+ asChild?: boolean;
39
+ }
40
+
41
+ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
42
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
43
+ const Comp = asChild ? Slot : "button";
44
+ return (
45
+ <Comp
46
+ className={cn(buttonVariants({ variant, size, className }))}
47
+ ref={ref}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+ );
53
+ Button.displayName = "Button";
54
+
55
+ export { Button, buttonVariants };