@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.
- package/CHANGELOG.md +153 -0
- package/bunfig.toml +2 -0
- package/package.json +40 -0
- package/src/components/Accordion.tsx +55 -0
- package/src/components/Alert.tsx +90 -0
- package/src/components/AmbientBackground.tsx +105 -0
- package/src/components/AnimatedCounter.tsx +54 -0
- package/src/components/BackLink.tsx +56 -0
- package/src/components/Badge.tsx +38 -0
- package/src/components/Button.tsx +55 -0
- package/src/components/Card.tsx +56 -0
- package/src/components/Checkbox.tsx +46 -0
- package/src/components/ColorPicker.tsx +69 -0
- package/src/components/CommandPalette.tsx +74 -0
- package/src/components/ConfirmationModal.tsx +134 -0
- package/src/components/DateRangeFilter.tsx +128 -0
- package/src/components/DateTimePicker.tsx +65 -0
- package/src/components/Dialog.tsx +134 -0
- package/src/components/DropdownMenu.tsx +96 -0
- package/src/components/DynamicForm/DynamicForm.tsx +126 -0
- package/src/components/DynamicForm/DynamicOptionsField.tsx +220 -0
- package/src/components/DynamicForm/FormField.tsx +690 -0
- package/src/components/DynamicForm/JsonField.tsx +98 -0
- package/src/components/DynamicForm/index.ts +11 -0
- package/src/components/DynamicForm/types.ts +95 -0
- package/src/components/DynamicForm/utils.ts +39 -0
- package/src/components/DynamicIcon.tsx +45 -0
- package/src/components/EditableText.tsx +141 -0
- package/src/components/EmptyState.tsx +32 -0
- package/src/components/HealthBadge.tsx +57 -0
- package/src/components/InfoBanner.tsx +97 -0
- package/src/components/Input.tsx +20 -0
- package/src/components/Label.tsx +17 -0
- package/src/components/LoadingSpinner.tsx +29 -0
- package/src/components/Markdown.tsx +206 -0
- package/src/components/NavItem.tsx +112 -0
- package/src/components/Page.tsx +58 -0
- package/src/components/PageLayout.tsx +83 -0
- package/src/components/PaginatedList.tsx +135 -0
- package/src/components/Pagination.tsx +195 -0
- package/src/components/PermissionDenied.tsx +31 -0
- package/src/components/PermissionGate.tsx +97 -0
- package/src/components/PluginConfigForm.tsx +91 -0
- package/src/components/SectionHeader.tsx +30 -0
- package/src/components/Select.tsx +157 -0
- package/src/components/StatusCard.tsx +78 -0
- package/src/components/StatusUpdateTimeline.tsx +222 -0
- package/src/components/StrategyConfigCard.tsx +333 -0
- package/src/components/SubscribeButton.tsx +96 -0
- package/src/components/Table.tsx +119 -0
- package/src/components/Tabs.tsx +141 -0
- package/src/components/TemplateEditor.test.ts +156 -0
- package/src/components/TemplateEditor.tsx +435 -0
- package/src/components/TerminalFeed.tsx +152 -0
- package/src/components/Textarea.tsx +22 -0
- package/src/components/ThemeProvider.tsx +76 -0
- package/src/components/Toast.tsx +118 -0
- package/src/components/ToastProvider.tsx +126 -0
- package/src/components/Toggle.tsx +47 -0
- package/src/components/Tooltip.tsx +20 -0
- package/src/components/UserMenu.tsx +79 -0
- package/src/hooks/usePagination.e2e.ts +275 -0
- package/src/hooks/usePagination.ts +231 -0
- package/src/index.ts +53 -0
- package/src/themes.css +204 -0
- package/src/utils/strip-markdown.ts +44 -0
- package/src/utils.ts +8 -0
- 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
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 };
|