@checkmate-monitor/ui 0.1.0

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 +77 -0
  2. package/bunfig.toml +2 -0
  3. package/package.json +39 -0
  4. package/src/components/Accordion.tsx +55 -0
  5. package/src/components/Alert.tsx +91 -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 +52 -0
  28. package/src/components/EditableText.tsx +141 -0
  29. package/src/components/EmptyState.tsx +32 -0
  30. package/src/components/HealthBadge.tsx +58 -0
  31. package/src/components/InfoBanner.tsx +98 -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 +76 -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,77 @@
1
+ # @checkmate-monitor/ui
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ffc28f6: ### Anonymous Role and Public Access
8
+
9
+ Introduces a configurable "anonymous" role for managing permissions available to unauthenticated users.
10
+
11
+ **Core Changes:**
12
+
13
+ - Added `userType: "public"` - endpoints accessible by both authenticated users (with their permissions) and anonymous users (with anonymous role permissions)
14
+ - Renamed `userType: "both"` to `"authenticated"` for clarity
15
+ - Renamed `isDefault` to `isAuthenticatedDefault` on Permission interface
16
+ - Added `isPublicDefault` flag for permissions that should be granted to the anonymous role by default
17
+
18
+ **Backend Infrastructure:**
19
+
20
+ - New `anonymous` system role created during auth-backend initialization
21
+ - New `disabled_public_default_permission` table tracks admin-disabled public defaults
22
+ - `autoAuthMiddleware` now checks anonymous role permissions for unauthenticated public endpoint access
23
+ - `AuthService.getAnonymousPermissions()` with 1-minute caching for performance
24
+ - Anonymous role filtered from `getRoles` endpoint (not assignable to users)
25
+ - Validation prevents assigning anonymous role to users
26
+
27
+ **Catalog Integration:**
28
+
29
+ - `catalog.read` permission now has both `isAuthenticatedDefault` and `isPublicDefault`
30
+ - Read endpoints (`getSystems`, `getGroups`, `getEntities`) now use `userType: "public"`
31
+
32
+ **UI:**
33
+
34
+ - New `PermissionGate` component for conditionally rendering content based on permissions
35
+
36
+ - b354ab3: # Strategy Instructions Support & Telegram Notification Plugin
37
+
38
+ ## Strategy Instructions Interface
39
+
40
+ 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:
41
+
42
+ - **`adminInstructions`**: Shown when admins configure platform-wide strategy settings (e.g., how to create API keys)
43
+ - **`userInstructions`**: Shown when users configure their personal settings (e.g., how to link their account)
44
+
45
+ ### Updated Components
46
+
47
+ - `StrategyConfigCard` now accepts an `instructions` prop and renders it before config sections
48
+ - `StrategyCard` passes `adminInstructions` to `StrategyConfigCard`
49
+ - `UserChannelCard` renders `userInstructions` when users need to connect
50
+
51
+ ## New Telegram Notification Plugin
52
+
53
+ Added `@checkmate-monitor/notification-telegram-backend` plugin for sending notifications via Telegram:
54
+
55
+ - Uses [grammY](https://grammy.dev/) framework for Telegram Bot API integration
56
+ - Sends messages with MarkdownV2 formatting and inline keyboard buttons for actions
57
+ - Includes comprehensive admin instructions for bot setup via @BotFather
58
+ - Includes user instructions for account linking
59
+
60
+ ### Configuration
61
+
62
+ Admins need to configure a Telegram Bot Token obtained from @BotFather.
63
+
64
+ ### User Linking
65
+
66
+ The strategy uses `contactResolution: { type: "custom" }` for Telegram Login Widget integration. Full frontend integration for the Login Widget is pending future work.
67
+
68
+ ### Patch Changes
69
+
70
+ - eff5b4e: Add standalone maintenance scheduling plugin
71
+
72
+ - New `@checkmate-monitor/maintenance-common` package with Zod schemas, permissions, oRPC contract, and extension slots
73
+ - New `@checkmate-monitor/maintenance-backend` package with Drizzle schema, service, and oRPC router
74
+ - New `@checkmate-monitor/maintenance-frontend` package with admin page and system detail panel
75
+ - Shared `DateTimePicker` component added to `@checkmate-monitor/ui`
76
+ - Database migrations for maintenances, maintenance_systems, and maintenance_updates tables
77
+ - @checkmate-monitor/frontend-api@0.0.2
package/bunfig.toml ADDED
@@ -0,0 +1,2 @@
1
+ [test]
2
+ preload = ["@checkmate-monitor/test-utils-frontend/setup"]
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@checkmate-monitor/ui",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.ts",
6
+ "dependencies": {
7
+ "@checkmate-monitor/frontend-api": "workspace:*",
8
+ "@radix-ui/react-accordion": "^1.2.12",
9
+ "@radix-ui/react-dialog": "^1.1.15",
10
+ "@radix-ui/react-select": "^2.2.6",
11
+ "@radix-ui/react-slot": "^1.2.4",
12
+ "ajv": "^8.17.1",
13
+ "ajv-formats": "^3.0.1",
14
+ "class-variance-authority": "^0.7.1",
15
+ "clsx": "^2.1.0",
16
+ "date-fns": "^4.1.0",
17
+ "lucide-react": "^0.344.0",
18
+ "prismjs": "^1.29.0",
19
+ "react": "^18.2.0",
20
+ "react-markdown": "^10.1.0",
21
+ "react-router-dom": "^6.20.0",
22
+ "react-simple-code-editor": "^0.14.1",
23
+ "recharts": "^3.6.0",
24
+ "tailwind-merge": "^2.2.0"
25
+ },
26
+ "devDependencies": {
27
+ "typescript": "^5.0.0",
28
+ "@types/react": "^18.2.0",
29
+ "@checkmate-monitor/test-utils-frontend": "workspace:*",
30
+ "@checkmate-monitor/tsconfig": "workspace:*",
31
+ "@checkmate-monitor/scripts": "workspace:*"
32
+ },
33
+ "scripts": {
34
+ "typecheck": "tsc --noEmit",
35
+ "lint": "bun run lint:code",
36
+ "lint:code": "eslint . --max-warnings 0",
37
+ "test": "bun test"
38
+ }
39
+ }
@@ -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,91 @@
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-foreground",
9
+ warning: "bg-warning/10 border-warning/30 text-warning-foreground",
10
+ error:
11
+ "bg-destructive/10 border-destructive/30 text-destructive-foreground",
12
+ info: "bg-info/10 border-info/30 text-info-foreground",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "default",
17
+ },
18
+ });
19
+
20
+ export interface AlertProps
21
+ extends React.HTMLAttributes<HTMLDivElement>,
22
+ VariantProps<typeof alertVariants> {
23
+ children: React.ReactNode;
24
+ }
25
+
26
+ export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
27
+ ({ className, variant, children, ...props }, ref) => {
28
+ return (
29
+ <div
30
+ ref={ref}
31
+ role="alert"
32
+ className={alertVariants({ variant, className })}
33
+ {...props}
34
+ >
35
+ <div className="flex gap-3 items-center">{children}</div>
36
+ </div>
37
+ );
38
+ }
39
+ );
40
+
41
+ Alert.displayName = "Alert";
42
+
43
+ export const AlertIcon = React.forwardRef<
44
+ HTMLDivElement,
45
+ React.HTMLAttributes<HTMLDivElement>
46
+ >(({ className, children, ...props }, ref) => (
47
+ <div
48
+ ref={ref}
49
+ className={`flex-shrink-0 opacity-70 ${className || ""}`}
50
+ {...props}
51
+ >
52
+ {children}
53
+ </div>
54
+ ));
55
+
56
+ AlertIcon.displayName = "AlertIcon";
57
+
58
+ export const AlertContent = React.forwardRef<
59
+ HTMLDivElement,
60
+ React.HTMLAttributes<HTMLDivElement>
61
+ >(({ className, ...props }, ref) => (
62
+ <div ref={ref} className={`flex-1 space-y-1 ${className || ""}`} {...props} />
63
+ ));
64
+
65
+ AlertContent.displayName = "AlertContent";
66
+
67
+ export const AlertTitle = React.forwardRef<
68
+ HTMLHeadingElement,
69
+ React.HTMLAttributes<HTMLHeadingElement>
70
+ >(({ className, ...props }, ref) => (
71
+ <h5
72
+ ref={ref}
73
+ className={`font-semibold text-sm leading-tight ${className || ""}`}
74
+ {...props}
75
+ />
76
+ ));
77
+
78
+ AlertTitle.displayName = "AlertTitle";
79
+
80
+ export const AlertDescription = React.forwardRef<
81
+ HTMLParagraphElement,
82
+ React.HTMLAttributes<HTMLParagraphElement>
83
+ >(({ className, ...props }, ref) => (
84
+ <div
85
+ ref={ref}
86
+ className={`text-sm leading-relaxed opacity-90 ${className || ""}`}
87
+ {...props}
88
+ />
89
+ ));
90
+
91
+ 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-foreground hover:bg-success/20",
19
+ warning:
20
+ "border-transparent bg-warning/10 text-warning-foreground hover:bg-warning/20",
21
+ info: "border-transparent bg-info/10 text-info-foreground 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 };
@@ -0,0 +1,56 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const Card = ({
5
+ className,
6
+ ...props
7
+ }: React.HTMLAttributes<HTMLDivElement>) => (
8
+ <div
9
+ className={cn(
10
+ "rounded-lg border border-border bg-card text-card-foreground shadow-sm",
11
+ className
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+
17
+ export const CardHeader = ({
18
+ className,
19
+ ...props
20
+ }: React.HTMLAttributes<HTMLDivElement>) => (
21
+ <div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
22
+ );
23
+
24
+ export const CardTitle = ({
25
+ className,
26
+ ...props
27
+ }: React.HTMLAttributes<HTMLHeadingElement>) => (
28
+ <h3
29
+ className={cn(
30
+ "text-2xl font-semibold leading-none tracking-tight",
31
+ className
32
+ )}
33
+ {...props}
34
+ />
35
+ );
36
+
37
+ export const CardDescription = ({
38
+ className,
39
+ ...props
40
+ }: React.HTMLAttributes<HTMLParagraphElement>) => (
41
+ <p className={cn("text-sm text-muted-foreground", className)} {...props} />
42
+ );
43
+
44
+ export const CardContent = ({
45
+ className,
46
+ ...props
47
+ }: React.HTMLAttributes<HTMLDivElement>) => (
48
+ <div className={cn("p-6 pt-0", className)} {...props} />
49
+ );
50
+
51
+ export const CardFooter = ({
52
+ className,
53
+ ...props
54
+ }: React.HTMLAttributes<HTMLDivElement>) => (
55
+ <div className={cn("flex items-center p-6 pt-0", className)} {...props} />
56
+ );
@@ -0,0 +1,46 @@
1
+ import React from "react";
2
+ import { Check } from "lucide-react";
3
+ import { cn } from "../utils";
4
+
5
+ interface CheckboxProps extends React.InputHTMLAttributes<HTMLInputElement> {
6
+ onCheckedChange?: (checked: boolean) => void;
7
+ }
8
+
9
+ export const Checkbox: React.FC<CheckboxProps> = ({
10
+ className,
11
+ checked,
12
+ onCheckedChange,
13
+ ...props
14
+ }) => {
15
+ // Compute styles to avoid nested ternary
16
+ const getBackgroundStyles = () => {
17
+ if (props.disabled) {
18
+ return "bg-muted border-border cursor-not-allowed";
19
+ }
20
+ if (checked) {
21
+ return "bg-primary border-primary cursor-pointer";
22
+ }
23
+ return "bg-background border-input cursor-pointer";
24
+ };
25
+
26
+ return (
27
+ <div
28
+ className={cn(
29
+ "peer h-4 w-4 shrink-0 rounded-sm border ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 flex items-center justify-center transition-colors",
30
+ getBackgroundStyles(),
31
+ className
32
+ )}
33
+ onClick={() => !props.disabled && onCheckedChange?.(!checked)}
34
+ >
35
+ {checked && (
36
+ <Check
37
+ className={cn(
38
+ "h-3 w-3",
39
+ props.disabled ? "text-muted-foreground" : "text-primary-foreground"
40
+ )}
41
+ strokeWidth={3}
42
+ />
43
+ )}
44
+ </div>
45
+ );
46
+ };