@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
@@ -0,0 +1,206 @@
1
+ import ReactMarkdown from "react-markdown";
2
+ import type { Components } from "react-markdown";
3
+ import { cn } from "../utils";
4
+
5
+ export interface MarkdownProps {
6
+ /** The markdown content to render */
7
+ children: string;
8
+ /** Additional CSS classes for the container */
9
+ className?: string;
10
+ /** Size variant affecting text size */
11
+ size?: "sm" | "base" | "lg";
12
+ }
13
+
14
+ /**
15
+ * Styled markdown renderer using Tailwind.
16
+ *
17
+ * Renders markdown content with consistent styling that matches
18
+ * the application's design system.
19
+ *
20
+ * @example
21
+ * ```tsx
22
+ * <Markdown>**Bold** and *italic* text</Markdown>
23
+ * <Markdown size="sm">Small text with [links](url)</Markdown>
24
+ * ```
25
+ */
26
+ export function Markdown({
27
+ children,
28
+ className,
29
+ size = "base",
30
+ }: MarkdownProps) {
31
+ const sizeClasses = {
32
+ sm: "text-sm",
33
+ base: "text-base",
34
+ lg: "text-lg",
35
+ };
36
+
37
+ const components: Components = {
38
+ // Paragraphs - no extra margin for inline use
39
+ p: ({ children }) => <span>{children}</span>,
40
+
41
+ // Bold text
42
+ strong: ({ children }) => (
43
+ <strong className="font-semibold text-foreground">{children}</strong>
44
+ ),
45
+
46
+ // Italic text
47
+ em: ({ children }) => <em className="italic">{children}</em>,
48
+
49
+ // Links
50
+ a: ({ href, children }) => (
51
+ <a
52
+ href={href}
53
+ className="text-primary hover:underline"
54
+ target="_blank"
55
+ rel="noopener noreferrer"
56
+ >
57
+ {children}
58
+ </a>
59
+ ),
60
+
61
+ // Inline code
62
+ code: ({ children }) => (
63
+ <code className="px-1 py-0.5 rounded bg-muted font-mono text-[0.9em]">
64
+ {children}
65
+ </code>
66
+ ),
67
+
68
+ // Strikethrough
69
+ del: ({ children }) => (
70
+ <del className="line-through text-muted-foreground">{children}</del>
71
+ ),
72
+ };
73
+
74
+ return (
75
+ <span className={cn(sizeClasses[size], className)}>
76
+ <ReactMarkdown components={components}>{children}</ReactMarkdown>
77
+ </span>
78
+ );
79
+ }
80
+
81
+ export interface MarkdownBlockProps extends MarkdownProps {
82
+ /** Whether to show as a prose block with proper spacing */
83
+ prose?: boolean;
84
+ }
85
+
86
+ /**
87
+ * Styled markdown block renderer for longer content.
88
+ *
89
+ * Unlike `Markdown`, this renders full blocks with proper paragraph
90
+ * spacing, headers, lists, and other block-level elements.
91
+ *
92
+ * @example
93
+ * ```tsx
94
+ * <MarkdownBlock>
95
+ * # Heading
96
+ *
97
+ * Paragraph with **bold** text.
98
+ *
99
+ * - List item 1
100
+ * - List item 2
101
+ * </MarkdownBlock>
102
+ * ```
103
+ */
104
+ export function MarkdownBlock({
105
+ children,
106
+ className,
107
+ size = "base",
108
+ prose = true,
109
+ }: MarkdownBlockProps) {
110
+ const sizeClasses = {
111
+ sm: "text-sm",
112
+ base: "text-base",
113
+ lg: "text-lg",
114
+ };
115
+
116
+ const components: Components = {
117
+ // Paragraphs with proper spacing
118
+ p: ({ children }) => <p className="mb-4 last:mb-0">{children}</p>,
119
+
120
+ // Headers
121
+ h1: ({ children }) => (
122
+ <h1 className="text-2xl font-bold mb-4 text-foreground">{children}</h1>
123
+ ),
124
+ h2: ({ children }) => (
125
+ <h2 className="text-xl font-semibold mb-3 text-foreground">{children}</h2>
126
+ ),
127
+ h3: ({ children }) => (
128
+ <h3 className="text-lg font-semibold mb-2 text-foreground">{children}</h3>
129
+ ),
130
+
131
+ // Bold text
132
+ strong: ({ children }) => (
133
+ <strong className="font-semibold text-foreground">{children}</strong>
134
+ ),
135
+
136
+ // Italic text
137
+ em: ({ children }) => <em className="italic">{children}</em>,
138
+
139
+ // Links
140
+ a: ({ href, children }) => (
141
+ <a
142
+ href={href}
143
+ className="text-primary hover:underline"
144
+ target="_blank"
145
+ rel="noopener noreferrer"
146
+ >
147
+ {children}
148
+ </a>
149
+ ),
150
+
151
+ // Lists
152
+ ul: ({ children }) => (
153
+ <ul className="list-disc list-inside mb-4 space-y-1">{children}</ul>
154
+ ),
155
+ ol: ({ children }) => (
156
+ <ol className="list-decimal list-inside mb-4 space-y-1">{children}</ol>
157
+ ),
158
+ li: ({ children }) => <li>{children}</li>,
159
+
160
+ // Blockquotes
161
+ blockquote: ({ children }) => (
162
+ <blockquote className="border-l-4 border-muted pl-4 italic text-muted-foreground mb-4">
163
+ {children}
164
+ </blockquote>
165
+ ),
166
+
167
+ // Code blocks
168
+ pre: ({ children }) => (
169
+ <pre className="bg-muted p-4 rounded-lg overflow-x-auto mb-4 font-mono text-sm">
170
+ {children}
171
+ </pre>
172
+ ),
173
+ code: ({ children, className }) => {
174
+ // Check if this is a code block (has language class)
175
+ const isBlock = className?.includes("language-");
176
+ if (isBlock) {
177
+ return <code>{children}</code>;
178
+ }
179
+ return (
180
+ <code className="px-1 py-0.5 rounded bg-muted font-mono text-[0.9em]">
181
+ {children}
182
+ </code>
183
+ );
184
+ },
185
+
186
+ // Horizontal rule
187
+ hr: () => <hr className="border-border my-6" />,
188
+
189
+ // Strikethrough
190
+ del: ({ children }) => (
191
+ <del className="line-through text-muted-foreground">{children}</del>
192
+ ),
193
+ };
194
+
195
+ return (
196
+ <div
197
+ className={cn(
198
+ sizeClasses[size],
199
+ prose && "prose prose-neutral dark:prose-invert max-w-none",
200
+ className
201
+ )}
202
+ >
203
+ <ReactMarkdown components={components}>{children}</ReactMarkdown>
204
+ </div>
205
+ );
206
+ }
@@ -0,0 +1,112 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { NavLink } from "react-router-dom";
3
+ import { ChevronDown } from "lucide-react";
4
+ import { cn } from "../utils";
5
+ import { useApi, permissionApiRef } from "@checkstack/frontend-api";
6
+
7
+ export interface NavItemProps {
8
+ to?: string;
9
+ label: string;
10
+ icon?: React.ReactNode;
11
+ permission?: string;
12
+ children?: React.ReactNode;
13
+ className?: string;
14
+ }
15
+
16
+ export const NavItem: React.FC<NavItemProps> = ({
17
+ to,
18
+ label,
19
+ icon,
20
+ permission,
21
+ children,
22
+ className,
23
+ }) => {
24
+ const [isOpen, setIsOpen] = useState(false);
25
+ const containerRef = useRef<HTMLDivElement>(null);
26
+
27
+ // Always call hooks at top level
28
+ // We assume permissionApi is available if we use it. Safe fallback?
29
+ // ApiProvider guarantees it if registered. App.tsx registers a default.
30
+ const permissionApi = useApi(permissionApiRef);
31
+ const { allowed, loading } = permissionApi.usePermission(permission || "");
32
+ const hasAccess = permission ? allowed : true;
33
+
34
+ // Handle click outside for dropdown
35
+ useEffect(() => {
36
+ const handleClickOutside = (event: MouseEvent) => {
37
+ if (
38
+ containerRef.current &&
39
+ !containerRef.current.contains(event.target as Node)
40
+ ) {
41
+ setIsOpen(false);
42
+ }
43
+ };
44
+ document.addEventListener("mousedown", handleClickOutside);
45
+ return () => document.removeEventListener("mousedown", handleClickOutside);
46
+ }, []);
47
+
48
+ if (loading || !hasAccess) return <></>;
49
+
50
+ const baseClasses = cn(
51
+ "flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors cursor-pointer",
52
+ "text-foreground hover:bg-accent hover:text-accent-foreground",
53
+ "focus:outline-none focus:ring-2 focus:ring-ring/50",
54
+ className
55
+ );
56
+
57
+ const activeClasses = "bg-accent text-accent-foreground";
58
+
59
+ // Dropdown / Parent Item
60
+ if (children) {
61
+ // Check if any child is active to highlight parent
62
+ // This is naive; normally we'd check paths.
63
+ // For now, let's just rely on click state or strict path matching if 'to' is present on parent.
64
+
65
+ return (
66
+ <div className="relative group" ref={containerRef}>
67
+ <button
68
+ onClick={() => setIsOpen(!isOpen)}
69
+ className={cn(baseClasses, isOpen && activeClasses)}
70
+ aria-expanded={isOpen}
71
+ >
72
+ {icon && <span className="w-4 h-4">{icon}</span>}
73
+ <span>{label}</span>
74
+ <ChevronDown
75
+ className={cn(
76
+ "w-4 h-4 transition-transform",
77
+ isOpen ? "rotate-180" : ""
78
+ )}
79
+ />
80
+ </button>
81
+
82
+ {isOpen && (
83
+ <div className="absolute left-0 mt-1 w-48 rounded-md shadow-lg bg-popover ring-1 ring-border z-50 animate-in fade-in zoom-in-95 duration-100">
84
+ <div className="py-1 flex flex-col p-1 gap-1">{children}</div>
85
+ </div>
86
+ )}
87
+ </div>
88
+ );
89
+ }
90
+
91
+ // Leaf Item (Link)
92
+ if (to) {
93
+ return (
94
+ <NavLink
95
+ to={to}
96
+ className={({ isActive }) => cn(baseClasses, isActive && activeClasses)}
97
+ end
98
+ >
99
+ {icon && <span className="w-4 h-4">{icon}</span>}
100
+ <span>{label}</span>
101
+ </NavLink>
102
+ );
103
+ }
104
+
105
+ // Fallback (just a label?)
106
+ return (
107
+ <div className={baseClasses}>
108
+ {icon && <span className="w-4 h-4">{icon}</span>}
109
+ <span>{label}</span>
110
+ </div>
111
+ );
112
+ };
@@ -0,0 +1,58 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ interface PageProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ children: React.ReactNode;
6
+ }
7
+
8
+ export const Page = React.forwardRef<HTMLDivElement, PageProps>(
9
+ ({ className, children, ...props }, ref) => (
10
+ <div
11
+ ref={ref}
12
+ className={cn("flex flex-col w-full h-full", className)}
13
+ {...props}
14
+ >
15
+ {children}
16
+ </div>
17
+ )
18
+ );
19
+ Page.displayName = "Page";
20
+
21
+ interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
22
+ title: string;
23
+ subtitle?: string;
24
+ actions?: React.ReactNode;
25
+ }
26
+
27
+ export const PageHeader = React.forwardRef<HTMLDivElement, PageHeaderProps>(
28
+ ({ className, title, subtitle, actions, ...props }, ref) => (
29
+ <div
30
+ ref={ref}
31
+ className={cn(
32
+ "flex flex-col md:flex-row items-center justify-between p-6 pb-2",
33
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ <div className="space-y-1">
38
+ <h2 className="text-2xl font-bold tracking-tight">{title}</h2>
39
+ {subtitle && <p className="text-muted-foreground">{subtitle}</p>}
40
+ </div>
41
+ {actions && <div className="flex items-center space-x-2">{actions}</div>}
42
+ </div>
43
+ )
44
+ );
45
+ PageHeader.displayName = "PageHeader";
46
+
47
+ interface PageContentProps extends React.HTMLAttributes<HTMLDivElement> {
48
+ children: React.ReactNode;
49
+ }
50
+
51
+ export const PageContent = React.forwardRef<HTMLDivElement, PageContentProps>(
52
+ ({ className, children, ...props }, ref) => (
53
+ <div ref={ref} className={cn("flex-1 p-6 pt-2", className)} {...props}>
54
+ {children}
55
+ </div>
56
+ )
57
+ );
58
+ PageContent.displayName = "PageContent";
@@ -0,0 +1,83 @@
1
+ import React from "react";
2
+ import {
3
+ Page,
4
+ PageHeader,
5
+ PageContent,
6
+ LoadingSpinner,
7
+ PermissionDenied,
8
+ } from "..";
9
+
10
+ interface PageLayoutProps {
11
+ title: string;
12
+ subtitle?: string;
13
+ actions?: React.ReactNode;
14
+ loading?: boolean;
15
+ allowed?: boolean;
16
+ children: React.ReactNode;
17
+ maxWidth?:
18
+ | "sm"
19
+ | "md"
20
+ | "lg"
21
+ | "xl"
22
+ | "2xl"
23
+ | "3xl"
24
+ | "4xl"
25
+ | "5xl"
26
+ | "6xl"
27
+ | "7xl"
28
+ | "full";
29
+ }
30
+
31
+ export const PageLayout: React.FC<PageLayoutProps> = ({
32
+ title,
33
+ subtitle,
34
+ actions,
35
+ loading,
36
+ allowed,
37
+ children,
38
+ maxWidth = "3xl",
39
+ }) => {
40
+ // If loading is explicitly true, show loading state
41
+ // If loading is undefined and allowed is false, also show loading state
42
+ // (this prevents "Access Denied" flash when permissions are still being fetched)
43
+ const isLoading =
44
+ loading === true || (loading === undefined && allowed === false);
45
+
46
+ if (isLoading) {
47
+ return (
48
+ <Page>
49
+ <PageHeader title={title} subtitle={subtitle} actions={actions} />
50
+ <PageContent>
51
+ <div className="flex justify-center py-12">
52
+ <LoadingSpinner />
53
+ </div>
54
+ </PageContent>
55
+ </Page>
56
+ );
57
+ }
58
+
59
+ // Only show permission denied when loading is explicitly false and allowed is false
60
+ if (allowed === false) {
61
+ return (
62
+ <Page>
63
+ <PageHeader title={title} subtitle={subtitle} actions={actions} />
64
+ <PageContent>
65
+ <PermissionDenied />
66
+ </PageContent>
67
+ </Page>
68
+ );
69
+ }
70
+
71
+ return (
72
+ <Page>
73
+ <PageHeader title={title} subtitle={subtitle} actions={actions} />
74
+ <PageContent>
75
+ <div
76
+ className={maxWidth === "full" ? "" : `max-w-${maxWidth} space-y-6`}
77
+ >
78
+ {children}
79
+ </div>
80
+ </PageContent>
81
+ </Page>
82
+ );
83
+ };
@@ -0,0 +1,135 @@
1
+ import * as React from "react";
2
+ import {
3
+ usePagination,
4
+ type UsePaginationOptions,
5
+ type PaginationState,
6
+ } from "../hooks/usePagination";
7
+ import { Pagination } from "./Pagination";
8
+ import { cn } from "../utils";
9
+
10
+ export interface PaginatedListProps<TResponse, TItem, TExtraParams = object>
11
+ extends UsePaginationOptions<TResponse, TItem, TExtraParams> {
12
+ /**
13
+ * Render function for the items
14
+ */
15
+ children: (
16
+ items: TItem[],
17
+ loading: boolean,
18
+ pagination: PaginationState
19
+ ) => React.ReactNode;
20
+
21
+ /**
22
+ * Show loading spinner
23
+ * @default true
24
+ */
25
+ showLoadingSpinner?: boolean;
26
+
27
+ /**
28
+ * Content to show when no items
29
+ */
30
+ emptyContent?: React.ReactNode;
31
+
32
+ /**
33
+ * Show pagination controls
34
+ * @default true
35
+ */
36
+ showPagination?: boolean;
37
+
38
+ /**
39
+ * Show page size selector
40
+ * @default true
41
+ */
42
+ showPageSize?: boolean;
43
+
44
+ /**
45
+ * Show total items count
46
+ * @default true
47
+ */
48
+ showTotal?: boolean;
49
+
50
+ /**
51
+ * Available page sizes
52
+ */
53
+ pageSizes?: number[];
54
+
55
+ /**
56
+ * Container class name
57
+ */
58
+ className?: string;
59
+
60
+ /**
61
+ * Pagination container class name
62
+ */
63
+ paginationClassName?: string;
64
+ }
65
+
66
+ /**
67
+ * All-in-one paginated list component with automatic data fetching.
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * <PaginatedList
72
+ * fetchFn={(p) => client.getNotifications(p)}
73
+ * getItems={(r) => r.notifications}
74
+ * getTotal={(r) => r.total}
75
+ * extraParams={{ unreadOnly: true }}
76
+ * >
77
+ * {(items, loading) =>
78
+ * loading ? null : items.map((item) => <Card key={item.id} {...item} />)
79
+ * }
80
+ * </PaginatedList>
81
+ * ```
82
+ */
83
+ export function PaginatedList<TResponse, TItem, TExtraParams = object>({
84
+ children,
85
+ showLoadingSpinner = true,
86
+ emptyContent,
87
+ showPagination = true,
88
+ showPageSize = true,
89
+ showTotal = true,
90
+ pageSizes,
91
+ className,
92
+ paginationClassName,
93
+ ...paginationOptions
94
+ }: PaginatedListProps<TResponse, TItem, TExtraParams>) {
95
+ const { items, loading, pagination } = usePagination(paginationOptions);
96
+
97
+ const showEmpty = !loading && items.length === 0 && emptyContent;
98
+
99
+ return (
100
+ <div className={cn("space-y-4", className)}>
101
+ {/* Loading state */}
102
+ {loading && showLoadingSpinner && (
103
+ <div className="flex justify-center py-8 text-muted-foreground">
104
+ Loading...
105
+ </div>
106
+ )}
107
+
108
+ {/* Empty state */}
109
+ {showEmpty && (
110
+ <div className="flex justify-center py-8 text-muted-foreground">
111
+ {emptyContent}
112
+ </div>
113
+ )}
114
+
115
+ {/* Content */}
116
+ {!showEmpty && children(items, loading, pagination)}
117
+
118
+ {/* Pagination controls */}
119
+ {showPagination && pagination.totalPages > 1 && (
120
+ <Pagination
121
+ page={pagination.page}
122
+ totalPages={pagination.totalPages}
123
+ onPageChange={pagination.setPage}
124
+ limit={pagination.limit}
125
+ onPageSizeChange={pagination.setLimit}
126
+ total={showTotal ? pagination.total : undefined}
127
+ showPageSize={showPageSize}
128
+ showTotal={showTotal}
129
+ pageSizes={pageSizes}
130
+ className={paginationClassName}
131
+ />
132
+ )}
133
+ </div>
134
+ );
135
+ }