@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
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../utils";
|
|
3
|
+
|
|
4
|
+
const Table = React.forwardRef<
|
|
5
|
+
HTMLTableElement,
|
|
6
|
+
React.HTMLAttributes<HTMLTableElement>
|
|
7
|
+
>(({ className, ...props }, ref) => (
|
|
8
|
+
<div className="relative w-full overflow-auto">
|
|
9
|
+
<table
|
|
10
|
+
ref={ref}
|
|
11
|
+
className={cn("w-full caption-bottom text-sm", className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
</div>
|
|
15
|
+
));
|
|
16
|
+
Table.displayName = "Table";
|
|
17
|
+
|
|
18
|
+
const TableHeader = React.forwardRef<
|
|
19
|
+
HTMLTableSectionElement,
|
|
20
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
21
|
+
>(({ className, ...props }, ref) => (
|
|
22
|
+
<thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} />
|
|
23
|
+
));
|
|
24
|
+
TableHeader.displayName = "TableHeader";
|
|
25
|
+
|
|
26
|
+
const TableBody = React.forwardRef<
|
|
27
|
+
HTMLTableSectionElement,
|
|
28
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
29
|
+
>(({ className, ...props }, ref) => (
|
|
30
|
+
<tbody
|
|
31
|
+
ref={ref}
|
|
32
|
+
className={cn("[&_tr:last-child]:border-0", className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
));
|
|
36
|
+
TableBody.displayName = "TableBody";
|
|
37
|
+
|
|
38
|
+
const TableFooter = React.forwardRef<
|
|
39
|
+
HTMLTableSectionElement,
|
|
40
|
+
React.HTMLAttributes<HTMLTableSectionElement>
|
|
41
|
+
>(({ className, ...props }, ref) => (
|
|
42
|
+
<tfoot
|
|
43
|
+
ref={ref}
|
|
44
|
+
className={cn(
|
|
45
|
+
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
|
46
|
+
className
|
|
47
|
+
)}
|
|
48
|
+
{...props}
|
|
49
|
+
/>
|
|
50
|
+
));
|
|
51
|
+
TableFooter.displayName = "TableFooter";
|
|
52
|
+
|
|
53
|
+
const TableRow = React.forwardRef<
|
|
54
|
+
HTMLTableRowElement,
|
|
55
|
+
React.HTMLAttributes<HTMLTableRowElement>
|
|
56
|
+
>(({ className, ...props }, ref) => (
|
|
57
|
+
<tr
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(
|
|
60
|
+
"border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted",
|
|
61
|
+
className
|
|
62
|
+
)}
|
|
63
|
+
{...props}
|
|
64
|
+
/>
|
|
65
|
+
));
|
|
66
|
+
TableRow.displayName = "TableRow";
|
|
67
|
+
|
|
68
|
+
const TableHead = React.forwardRef<
|
|
69
|
+
HTMLTableCellElement,
|
|
70
|
+
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
71
|
+
>(({ className, ...props }, ref) => (
|
|
72
|
+
<th
|
|
73
|
+
ref={ref}
|
|
74
|
+
className={cn(
|
|
75
|
+
"h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [button&]:text-muted-foreground",
|
|
76
|
+
className
|
|
77
|
+
)}
|
|
78
|
+
{...props}
|
|
79
|
+
/>
|
|
80
|
+
));
|
|
81
|
+
TableHead.displayName = "TableHead";
|
|
82
|
+
|
|
83
|
+
const TableCell = React.forwardRef<
|
|
84
|
+
HTMLTableCellElement,
|
|
85
|
+
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
86
|
+
>(({ className, ...props }, ref) => (
|
|
87
|
+
<td
|
|
88
|
+
ref={ref}
|
|
89
|
+
className={cn(
|
|
90
|
+
"p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
|
91
|
+
className
|
|
92
|
+
)}
|
|
93
|
+
{...props}
|
|
94
|
+
/>
|
|
95
|
+
));
|
|
96
|
+
TableCell.displayName = "TableCell";
|
|
97
|
+
|
|
98
|
+
const TableCaption = React.forwardRef<
|
|
99
|
+
HTMLTableCaptionElement,
|
|
100
|
+
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
101
|
+
>(({ className, ...props }, ref) => (
|
|
102
|
+
<caption
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
|
105
|
+
{...props}
|
|
106
|
+
/>
|
|
107
|
+
));
|
|
108
|
+
TableCaption.displayName = "TableCaption";
|
|
109
|
+
|
|
110
|
+
export {
|
|
111
|
+
Table,
|
|
112
|
+
TableHeader,
|
|
113
|
+
TableBody,
|
|
114
|
+
TableFooter,
|
|
115
|
+
TableHead,
|
|
116
|
+
TableRow,
|
|
117
|
+
TableCell,
|
|
118
|
+
TableCaption,
|
|
119
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
|
|
3
|
+
export interface TabItem {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface TabsProps {
|
|
10
|
+
items: TabItem[];
|
|
11
|
+
activeTab: string;
|
|
12
|
+
onTabChange: (tabId: string) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const Tabs: React.FC<TabsProps> = ({
|
|
17
|
+
items,
|
|
18
|
+
activeTab,
|
|
19
|
+
onTabChange,
|
|
20
|
+
className = "",
|
|
21
|
+
}) => {
|
|
22
|
+
const tabRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
23
|
+
|
|
24
|
+
const handleKeyDown = (event: React.KeyboardEvent, currentIndex: number) => {
|
|
25
|
+
let newIndex = currentIndex;
|
|
26
|
+
|
|
27
|
+
switch (event.key) {
|
|
28
|
+
case "ArrowLeft": {
|
|
29
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1;
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
case "ArrowRight": {
|
|
34
|
+
newIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0;
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
case "Home": {
|
|
39
|
+
newIndex = 0;
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
case "End": {
|
|
44
|
+
newIndex = items.length - 1;
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
default: {
|
|
49
|
+
// No action needed for other keys
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (newIndex !== currentIndex) {
|
|
55
|
+
onTabChange(items[newIndex].id);
|
|
56
|
+
tabRefs.current.get(items[newIndex].id)?.focus();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className={`relative ${className}`}>
|
|
62
|
+
<div
|
|
63
|
+
className="flex flex-col md:flex-row gap-1 p-1 bg-muted/30 rounded-lg"
|
|
64
|
+
role="tablist"
|
|
65
|
+
aria-label="Tabs"
|
|
66
|
+
>
|
|
67
|
+
{items.map((item, index) => {
|
|
68
|
+
const isActive = activeTab === item.id;
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
key={item.id}
|
|
72
|
+
ref={(el) => {
|
|
73
|
+
if (el) {
|
|
74
|
+
tabRefs.current.set(item.id, el);
|
|
75
|
+
} else {
|
|
76
|
+
tabRefs.current.delete(item.id);
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
onClick={() => onTabChange(item.id)}
|
|
80
|
+
onKeyDown={(e) => handleKeyDown(e, index)}
|
|
81
|
+
className={`
|
|
82
|
+
relative px-4 py-2.5 flex items-center justify-center md:justify-start gap-2 rounded-md
|
|
83
|
+
text-sm font-medium transition-all duration-200
|
|
84
|
+
focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/20 focus-visible:ring-offset-0
|
|
85
|
+
${
|
|
86
|
+
isActive
|
|
87
|
+
? "bg-primary text-primary-foreground shadow-md shadow-primary/30"
|
|
88
|
+
: "text-muted-foreground hover:text-foreground hover:bg-background/50"
|
|
89
|
+
}
|
|
90
|
+
`}
|
|
91
|
+
role="tab"
|
|
92
|
+
aria-selected={isActive}
|
|
93
|
+
aria-controls={`tabpanel-${item.id}`}
|
|
94
|
+
tabIndex={isActive ? 0 : -1}
|
|
95
|
+
>
|
|
96
|
+
{item.icon && (
|
|
97
|
+
<span
|
|
98
|
+
className={`transition-all duration-200 ${
|
|
99
|
+
isActive ? "scale-110" : ""
|
|
100
|
+
}`}
|
|
101
|
+
>
|
|
102
|
+
{item.icon}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<span>{item.label}</span>
|
|
106
|
+
</button>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export interface TabPanelProps {
|
|
115
|
+
id: string;
|
|
116
|
+
activeTab: string;
|
|
117
|
+
children: React.ReactNode;
|
|
118
|
+
className?: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const TabPanel: React.FC<TabPanelProps> = ({
|
|
122
|
+
id,
|
|
123
|
+
activeTab,
|
|
124
|
+
children,
|
|
125
|
+
className = "",
|
|
126
|
+
}) => {
|
|
127
|
+
const isActive = activeTab === id;
|
|
128
|
+
|
|
129
|
+
return isActive ? (
|
|
130
|
+
<div
|
|
131
|
+
id={`tabpanel-${id}`}
|
|
132
|
+
role="tabpanel"
|
|
133
|
+
aria-labelledby={`tab-${id}`}
|
|
134
|
+
className={`animate-in fade-in-0 duration-200 ${className}`}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</div>
|
|
138
|
+
) : (
|
|
139
|
+
<></>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { detectTemplateContext } from "./TemplateEditor";
|
|
3
|
+
|
|
4
|
+
describe("detectTemplateContext", () => {
|
|
5
|
+
describe("mustache syntax ({{}})", () => {
|
|
6
|
+
it("should detect context when cursor is immediately after {{", () => {
|
|
7
|
+
const result = detectTemplateContext("Hello {{", 8, "mustache");
|
|
8
|
+
expect(result).toEqual({
|
|
9
|
+
isInTemplate: true,
|
|
10
|
+
query: "",
|
|
11
|
+
startPos: 6,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("should detect context with partial query", () => {
|
|
16
|
+
const result = detectTemplateContext("Hello {{pay", 11, "mustache");
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
isInTemplate: true,
|
|
19
|
+
query: "pay",
|
|
20
|
+
startPos: 6,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("should detect context with full path being typed", () => {
|
|
25
|
+
const result = detectTemplateContext(
|
|
26
|
+
"Hello {{payload.title",
|
|
27
|
+
21,
|
|
28
|
+
"mustache"
|
|
29
|
+
);
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
isInTemplate: true,
|
|
32
|
+
query: "payload.title",
|
|
33
|
+
startPos: 6,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("should not detect context when template is closed", () => {
|
|
38
|
+
const result = detectTemplateContext(
|
|
39
|
+
"Hello {{name}} world",
|
|
40
|
+
20,
|
|
41
|
+
"mustache"
|
|
42
|
+
);
|
|
43
|
+
expect(result).toEqual({
|
|
44
|
+
isInTemplate: false,
|
|
45
|
+
query: "",
|
|
46
|
+
startPos: -1,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should not detect context when cursor is before {{", () => {
|
|
51
|
+
const result = detectTemplateContext("Hello {{name}}", 3, "mustache");
|
|
52
|
+
expect(result).toEqual({
|
|
53
|
+
isInTemplate: false,
|
|
54
|
+
query: "",
|
|
55
|
+
startPos: -1,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should detect context in second template when first is closed", () => {
|
|
60
|
+
const result = detectTemplateContext(
|
|
61
|
+
"{{first}} and {{second",
|
|
62
|
+
22,
|
|
63
|
+
"mustache"
|
|
64
|
+
);
|
|
65
|
+
expect(result).toEqual({
|
|
66
|
+
isInTemplate: true,
|
|
67
|
+
query: "second",
|
|
68
|
+
startPos: 14,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("should not detect context when query contains newline", () => {
|
|
73
|
+
const result = detectTemplateContext("Hello {{\nworld", 14, "mustache");
|
|
74
|
+
expect(result).toEqual({
|
|
75
|
+
isInTemplate: false,
|
|
76
|
+
query: "",
|
|
77
|
+
startPos: -1,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should handle empty string", () => {
|
|
82
|
+
const result = detectTemplateContext("", 0, "mustache");
|
|
83
|
+
expect(result).toEqual({
|
|
84
|
+
isInTemplate: false,
|
|
85
|
+
query: "",
|
|
86
|
+
startPos: -1,
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should handle text without any templates", () => {
|
|
91
|
+
const result = detectTemplateContext("Hello world", 11, "mustache");
|
|
92
|
+
expect(result).toEqual({
|
|
93
|
+
isInTemplate: false,
|
|
94
|
+
query: "",
|
|
95
|
+
startPos: -1,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("dollar syntax (${})", () => {
|
|
101
|
+
it("should detect context when cursor is immediately after ${", () => {
|
|
102
|
+
const result = detectTemplateContext("Hello ${", 8, "dollar");
|
|
103
|
+
expect(result).toEqual({
|
|
104
|
+
isInTemplate: true,
|
|
105
|
+
query: "",
|
|
106
|
+
startPos: 6,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should detect context with partial query", () => {
|
|
111
|
+
const result = detectTemplateContext("Hello ${user", 12, "dollar");
|
|
112
|
+
expect(result).toEqual({
|
|
113
|
+
isInTemplate: true,
|
|
114
|
+
query: "user",
|
|
115
|
+
startPos: 6,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should not detect context when template is closed", () => {
|
|
120
|
+
const result = detectTemplateContext("Hello ${name} world", 19, "dollar");
|
|
121
|
+
expect(result).toEqual({
|
|
122
|
+
isInTemplate: false,
|
|
123
|
+
query: "",
|
|
124
|
+
startPos: -1,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("edge cases", () => {
|
|
130
|
+
it("should handle nested braces correctly", () => {
|
|
131
|
+
const result = detectTemplateContext("{{outer.{{inner", 15, "mustache");
|
|
132
|
+
// Should find the last {{ which is at position 8
|
|
133
|
+
expect(result.isInTemplate).toBe(true);
|
|
134
|
+
expect(result.startPos).toBe(8);
|
|
135
|
+
expect(result.query).toBe("inner");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should handle cursor at start of template", () => {
|
|
139
|
+
const result = detectTemplateContext("{{", 2, "mustache");
|
|
140
|
+
expect(result).toEqual({
|
|
141
|
+
isInTemplate: true,
|
|
142
|
+
query: "",
|
|
143
|
+
startPos: 0,
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("should detect context after multiple closed templates", () => {
|
|
148
|
+
const result = detectTemplateContext("{{a}} {{b}} {{c", 15, "mustache");
|
|
149
|
+
expect(result).toEqual({
|
|
150
|
+
isInTemplate: true,
|
|
151
|
+
query: "c",
|
|
152
|
+
startPos: 12,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|