@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,98 @@
1
+ import React from "react";
2
+ import Ajv from "ajv";
3
+ import addFormats from "ajv-formats";
4
+ import Editor from "react-simple-code-editor";
5
+ // @ts-expect-error - prismjs doesn't have types for deep imports in some environments
6
+ import { highlight, languages } from "prismjs/components/prism-core";
7
+ import "prismjs/components/prism-json";
8
+
9
+ import type { JsonFieldProps } from "./types";
10
+
11
+ const ajv = new Ajv({ allErrors: true, strict: false });
12
+ addFormats(ajv);
13
+
14
+ /**
15
+ * A JSON editor field with syntax highlighting and schema validation.
16
+ */
17
+ export const JsonField: React.FC<JsonFieldProps> = ({
18
+ id,
19
+ value,
20
+ propSchema,
21
+ onChange,
22
+ }) => {
23
+ const [internalValue, setInternalValue] = React.useState(
24
+ JSON.stringify(value || {}, undefined, 2)
25
+ );
26
+ const [error, setError] = React.useState<string | undefined>();
27
+ const lastPropValue = React.useRef(value);
28
+
29
+ const validateFn = React.useMemo(() => {
30
+ try {
31
+ return ajv.compile(propSchema);
32
+ } catch {
33
+ return;
34
+ }
35
+ }, [propSchema]);
36
+
37
+ // Sync internal value ONLY when external value changes from outside
38
+ React.useEffect(() => {
39
+ const valueString = JSON.stringify(value);
40
+ const lastValueString = JSON.stringify(lastPropValue.current);
41
+
42
+ if (valueString !== lastValueString) {
43
+ setInternalValue(JSON.stringify(value || {}, undefined, 2));
44
+ lastPropValue.current = value;
45
+ setError(undefined);
46
+ }
47
+ }, [value]);
48
+
49
+ const validate = (val: string) => {
50
+ try {
51
+ const parsed = JSON.parse(val);
52
+ if (!validateFn) {
53
+ setError(undefined);
54
+ return parsed;
55
+ }
56
+
57
+ const valid = validateFn(parsed);
58
+ if (!valid) {
59
+ setError(
60
+ validateFn.errors
61
+ ?.map((e) => `${e.instancePath} ${e.message}`)
62
+ .join(", ")
63
+ );
64
+ return false;
65
+ }
66
+ setError(undefined);
67
+ return parsed;
68
+ } catch (error_: unknown) {
69
+ setError(`Invalid JSON: ${(error_ as Error).message}`);
70
+ return false;
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div className="space-y-2">
76
+ <div className="min-h-[100px] w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all overflow-hidden box-border">
77
+ <Editor
78
+ id={id}
79
+ value={internalValue}
80
+ onValueChange={(code) => {
81
+ setInternalValue(code);
82
+ const parsed = validate(code);
83
+ if (parsed !== false) {
84
+ lastPropValue.current = parsed;
85
+ onChange(parsed);
86
+ }
87
+ }}
88
+ highlight={(code) => highlight(code, languages.json)}
89
+ padding={10}
90
+ style={{
91
+ minHeight: "100px",
92
+ }}
93
+ />
94
+ </div>
95
+ {error && <p className="text-xs text-destructive font-medium">{error}</p>}
96
+ </div>
97
+ );
98
+ };
@@ -0,0 +1,11 @@
1
+ // Main component
2
+ export { DynamicForm } from "./DynamicForm";
3
+
4
+ // Types for external consumers
5
+ export type {
6
+ JsonSchema,
7
+ JsonSchemaProperty,
8
+ DynamicFormProps,
9
+ OptionsResolver,
10
+ ResolverOption,
11
+ } from "./types";
@@ -0,0 +1,95 @@
1
+ import type { TemplateProperty } from "../TemplateEditor";
2
+
3
+ export interface JsonSchemaProperty {
4
+ type?: string;
5
+ description?: string;
6
+ enum?: string[];
7
+ const?: string | number | boolean; // For discriminator values
8
+ properties?: Record<string, JsonSchemaProperty>;
9
+ items?: JsonSchemaProperty;
10
+ required?: string[];
11
+ additionalProperties?: boolean | JsonSchemaProperty;
12
+ format?: string;
13
+ default?: unknown;
14
+ oneOf?: JsonSchemaProperty[]; // Discriminated union variants
15
+ anyOf?: JsonSchemaProperty[]; // Union variants
16
+ "x-secret"?: boolean; // Custom metadata for secret fields
17
+ "x-color"?: boolean; // Custom metadata for color fields
18
+ "x-options-resolver"?: string; // Name of a resolver function for dynamic options
19
+ "x-depends-on"?: string[]; // Field names this field depends on (triggers refetch when they change)
20
+ "x-hidden"?: boolean; // Field should be hidden in form (auto-populated)
21
+ "x-searchable"?: boolean; // Shows a search input for filtering dropdown options
22
+ }
23
+
24
+ /** Option returned by an options resolver */
25
+ export interface ResolverOption {
26
+ value: string;
27
+ label: string;
28
+ }
29
+
30
+ /** Function that resolves dynamic options, receives form values as context */
31
+ export type OptionsResolver = (
32
+ formValues: Record<string, unknown>
33
+ ) => Promise<ResolverOption[]>;
34
+
35
+ export interface JsonSchema {
36
+ properties?: Record<string, JsonSchemaProperty>;
37
+ required?: string[];
38
+ }
39
+
40
+ export interface DynamicFormProps {
41
+ schema: JsonSchema;
42
+ value: Record<string, unknown>;
43
+ onChange: (value: Record<string, unknown>) => void;
44
+ /**
45
+ * Optional callback when form validity changes.
46
+ * Reports true if all required fields are filled.
47
+ */
48
+ onValidChange?: (isValid: boolean) => void;
49
+ /**
50
+ * Optional map of resolver names to functions that fetch dynamic options.
51
+ * Referenced by x-options-resolver in schema properties.
52
+ */
53
+ optionsResolvers?: Record<string, OptionsResolver>;
54
+ /**
55
+ * Optional list of available template properties for template fields.
56
+ * Passed to TemplateEditor for autocompletion hints.
57
+ */
58
+ templateProperties?: TemplateProperty[];
59
+ }
60
+
61
+ /** Props for the FormField component */
62
+ export interface FormFieldProps {
63
+ id: string;
64
+ label: string;
65
+ propSchema: JsonSchemaProperty;
66
+ value: unknown;
67
+ isRequired?: boolean;
68
+ formValues: Record<string, unknown>;
69
+ optionsResolvers?: Record<string, OptionsResolver>;
70
+ templateProperties?: TemplateProperty[];
71
+ onChange: (val: unknown) => void;
72
+ }
73
+
74
+ /** Props for the DynamicOptionsField component */
75
+ export interface DynamicOptionsFieldProps {
76
+ id: string;
77
+ label: string;
78
+ description?: string;
79
+ value: unknown;
80
+ isRequired?: boolean;
81
+ resolverName: string;
82
+ dependsOn?: string[];
83
+ searchable?: boolean;
84
+ formValues: Record<string, unknown>;
85
+ optionsResolvers: Record<string, OptionsResolver>;
86
+ onChange: (val: unknown) => void;
87
+ }
88
+
89
+ /** Props for the JsonField component */
90
+ export interface JsonFieldProps {
91
+ id: string;
92
+ value: Record<string, unknown>;
93
+ propSchema: JsonSchemaProperty;
94
+ onChange: (val: Record<string, unknown>) => void;
95
+ }
@@ -0,0 +1,39 @@
1
+ import type { JsonSchema } from "./types";
2
+
3
+ /**
4
+ * Cleans a description string by removing textarea markers.
5
+ * Returns undefined if the description is empty or just "textarea".
6
+ */
7
+ export const getCleanDescription = (
8
+ description?: string
9
+ ): string | undefined => {
10
+ if (!description || description === "textarea") return;
11
+ const cleaned = description.replace("[textarea]", "").trim();
12
+ if (!cleaned) return;
13
+ return cleaned;
14
+ };
15
+
16
+ /**
17
+ * Extracts default values from a JSON schema recursively.
18
+ */
19
+ export const extractDefaults = (
20
+ schema: JsonSchema
21
+ ): Record<string, unknown> => {
22
+ const defaults: Record<string, unknown> = {};
23
+
24
+ if (!schema.properties) return defaults;
25
+
26
+ for (const [key, propSchema] of Object.entries(schema.properties)) {
27
+ if (propSchema.default !== undefined) {
28
+ defaults[key] = propSchema.default;
29
+ } else if (propSchema.type === "object" && propSchema.properties) {
30
+ // Recursively extract defaults for nested objects
31
+ defaults[key] = extractDefaults(propSchema as JsonSchema);
32
+ } else if (propSchema.type === "array") {
33
+ // Arrays default to empty array
34
+ defaults[key] = [];
35
+ }
36
+ }
37
+
38
+ return defaults;
39
+ };
@@ -0,0 +1,45 @@
1
+ import { icons, type LucideIcon } from "lucide-react";
2
+ import { Settings } from "lucide-react";
3
+ import type { LucideIconName } from "@checkstack/common";
4
+
5
+ // Re-export the type for convenience
6
+ export type { LucideIconName } from "@checkstack/common";
7
+
8
+ /**
9
+ * Props for the DynamicIcon component
10
+ */
11
+ export interface DynamicIconProps {
12
+ /** Lucide icon name in PascalCase (e.g., 'AlertCircle', 'HeartPulse') */
13
+ name?: LucideIconName;
14
+ /** CSS class name to apply to the icon */
15
+ className?: string;
16
+ /** Fallback icon if name is not provided */
17
+ fallback?: LucideIcon;
18
+ }
19
+
20
+ /**
21
+ * Dynamically renders a Lucide icon by name.
22
+ * Falls back to Settings icon if the icon name is not provided.
23
+ *
24
+ * @example
25
+ * <DynamicIcon name="AlertCircle" />
26
+ * <DynamicIcon name="HeartPulse" className="h-6 w-6" />
27
+ */
28
+ export function DynamicIcon({
29
+ name,
30
+ className = "h-5 w-5",
31
+ fallback: FallbackIcon = Settings,
32
+ }: DynamicIconProps) {
33
+ if (!name) {
34
+ return <FallbackIcon className={className} />;
35
+ }
36
+
37
+ const Icon = icons[name];
38
+
39
+ // Fallback if icon name doesn't exist in lucide-react
40
+ if (!Icon) {
41
+ return <FallbackIcon className={className} />;
42
+ }
43
+
44
+ return <Icon className={className} />;
45
+ }
@@ -0,0 +1,141 @@
1
+ import React, { useState, useRef, useEffect } from "react";
2
+ import { cn } from "../utils";
3
+ import { Input } from "./Input";
4
+ import { Button } from "./Button";
5
+ import { Check, X } from "lucide-react";
6
+
7
+ export interface EditableTextProps {
8
+ value: string;
9
+ onSave: (newValue: string) => Promise<void> | void;
10
+ className?: string;
11
+ inputClassName?: string;
12
+ placeholder?: string;
13
+ validate?: (value: string) => boolean;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ export const EditableText: React.FC<EditableTextProps> = ({
18
+ value,
19
+ onSave,
20
+ className,
21
+ inputClassName,
22
+ placeholder,
23
+ validate,
24
+ disabled = false,
25
+ }) => {
26
+ const [isEditing, setIsEditing] = useState(false);
27
+ const [editValue, setEditValue] = useState(value);
28
+ const [isSaving, setIsSaving] = useState(false);
29
+ const inputRef = useRef<HTMLInputElement>(null);
30
+
31
+ useEffect(() => {
32
+ if (isEditing && inputRef.current) {
33
+ inputRef.current.focus();
34
+ inputRef.current.select();
35
+ }
36
+ }, [isEditing]);
37
+
38
+ const handleEdit = () => {
39
+ setEditValue(value);
40
+ setIsEditing(true);
41
+ };
42
+
43
+ const handleCancel = () => {
44
+ setEditValue(value);
45
+ setIsEditing(false);
46
+ };
47
+
48
+ const handleSave = async () => {
49
+ const trimmedValue = editValue.trim();
50
+
51
+ if (!trimmedValue) {
52
+ return;
53
+ }
54
+
55
+ if (validate && !validate(trimmedValue)) {
56
+ return;
57
+ }
58
+
59
+ if (trimmedValue === value) {
60
+ setIsEditing(false);
61
+ return;
62
+ }
63
+
64
+ setIsSaving(true);
65
+ try {
66
+ await onSave(trimmedValue);
67
+ setIsEditing(false);
68
+ } catch (error) {
69
+ console.error("Failed to save:", error);
70
+ } finally {
71
+ setIsSaving(false);
72
+ }
73
+ };
74
+
75
+ const handleKeyDown = (e: React.KeyboardEvent) => {
76
+ if (e.key === "Enter") {
77
+ e.preventDefault();
78
+ handleSave();
79
+ } else if (e.key === "Escape") {
80
+ e.preventDefault();
81
+ handleCancel();
82
+ }
83
+ };
84
+
85
+ if (isEditing) {
86
+ return (
87
+ <div className={cn("flex items-center gap-2", className)}>
88
+ <Input
89
+ ref={inputRef}
90
+ value={editValue}
91
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
92
+ setEditValue(e.target.value)
93
+ }
94
+ onKeyDown={handleKeyDown}
95
+ placeholder={placeholder}
96
+ disabled={isSaving}
97
+ className={cn("flex-1", inputClassName)}
98
+ />
99
+ <Button
100
+ variant="ghost"
101
+ className="h-8 w-8 p-0 text-green-600 dark:text-green-400 hover:text-green-700 dark:hover:text-green-300 hover:bg-green-50 dark:hover:bg-green-950/20"
102
+ onClick={handleSave}
103
+ disabled={isSaving || !editValue.trim()}
104
+ type="button"
105
+ >
106
+ <Check className="w-4 h-4" />
107
+ </Button>
108
+ <Button
109
+ variant="ghost"
110
+ className="h-8 w-8 p-0 text-muted-foreground hover:text-foreground hover:bg-accent"
111
+ onClick={handleCancel}
112
+ disabled={isSaving}
113
+ type="button"
114
+ >
115
+ <X className="w-4 h-4" />
116
+ </Button>
117
+ </div>
118
+ );
119
+ }
120
+
121
+ return (
122
+ <div
123
+ className={cn(
124
+ "cursor-pointer hover:underline hover:decoration-dashed hover:decoration-1 hover:underline-offset-4 transition-all",
125
+ disabled && "cursor-default hover:no-underline opacity-50",
126
+ className
127
+ )}
128
+ onClick={disabled ? undefined : handleEdit}
129
+ role="button"
130
+ tabIndex={disabled ? -1 : 0}
131
+ onKeyDown={(e) => {
132
+ if (!disabled && (e.key === "Enter" || e.key === " ")) {
133
+ e.preventDefault();
134
+ handleEdit();
135
+ }
136
+ }}
137
+ >
138
+ {value}
139
+ </div>
140
+ );
141
+ };
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import { Card, CardContent } from "./Card";
3
+ import { cn } from "../utils";
4
+
5
+ interface EmptyStateProps extends React.HTMLAttributes<HTMLDivElement> {
6
+ title: string;
7
+ description?: string;
8
+ icon?: React.ReactNode;
9
+ }
10
+
11
+ export const EmptyState: React.FC<EmptyStateProps> = ({
12
+ title,
13
+ description,
14
+ icon,
15
+ className,
16
+ ...props
17
+ }) => {
18
+ return (
19
+ <Card
20
+ className={cn("border-dashed border-2 bg-muted/30", className)}
21
+ {...props}
22
+ >
23
+ <CardContent className="flex flex-col items-center justify-center py-12 text-center">
24
+ {icon && <div className="text-muted-foreground/40 mb-4">{icon}</div>}
25
+ <p className="text-foreground font-medium">{title}</p>
26
+ {description && (
27
+ <p className="text-sm text-muted-foreground mt-1">{description}</p>
28
+ )}
29
+ </CardContent>
30
+ </Card>
31
+ );
32
+ };
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+ import { CheckCircle2, AlertTriangle, XCircle } from "lucide-react";
3
+ import { cn } from "../utils";
4
+
5
+ export type HealthStatus = "healthy" | "degraded" | "unhealthy";
6
+
7
+ interface HealthBadgeProps {
8
+ status: HealthStatus;
9
+ variant?: "compact" | "full";
10
+ showIcon?: boolean;
11
+ className?: string;
12
+ }
13
+
14
+ const statusConfig = {
15
+ healthy: {
16
+ icon: CheckCircle2,
17
+ label: "Healthy",
18
+ className: "bg-success/10 text-success border-success/30",
19
+ iconClassName: "text-success",
20
+ },
21
+ degraded: {
22
+ icon: AlertTriangle,
23
+ label: "Degraded",
24
+ className: "bg-warning/10 text-warning border-warning/30",
25
+ iconClassName: "text-warning",
26
+ },
27
+ unhealthy: {
28
+ icon: XCircle,
29
+ label: "Unhealthy",
30
+ className: "bg-destructive/10 text-destructive border-destructive/30",
31
+ iconClassName: "text-destructive",
32
+ },
33
+ } as const;
34
+
35
+ export const HealthBadge: React.FC<HealthBadgeProps> = ({
36
+ status,
37
+ variant = "full",
38
+ showIcon = true,
39
+ className,
40
+ }) => {
41
+ const config = statusConfig[status];
42
+ const Icon = config.icon;
43
+ const isCompact = variant === "compact";
44
+
45
+ return (
46
+ <span
47
+ className={cn(
48
+ "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs font-medium transition-colors",
49
+ config.className,
50
+ className
51
+ )}
52
+ >
53
+ {showIcon && <Icon className={cn("h-3.5 w-3.5", config.iconClassName)} />}
54
+ {!isCompact && <span>{config.label}</span>}
55
+ </span>
56
+ );
57
+ };
@@ -0,0 +1,97 @@
1
+ import React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ const infoBannerVariants = cva(
5
+ "relative w-full rounded-lg border px-3 py-2.5 text-sm",
6
+ {
7
+ variants: {
8
+ variant: {
9
+ default: "bg-muted/30 border-border/50 text-muted-foreground",
10
+ info: "bg-info/5 border-info/20 text-info",
11
+ warning: "bg-warning/5 border-warning/20 text-warning",
12
+ success: "bg-success/5 border-success/20 text-success",
13
+ error: "bg-destructive/5 border-destructive/20 text-destructive",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ );
21
+
22
+ export interface InfoBannerProps
23
+ extends React.HTMLAttributes<HTMLDivElement>,
24
+ VariantProps<typeof infoBannerVariants> {
25
+ children: React.ReactNode;
26
+ }
27
+
28
+ export const InfoBanner = React.forwardRef<HTMLDivElement, InfoBannerProps>(
29
+ ({ className, variant, children, ...props }, ref) => {
30
+ return (
31
+ <div
32
+ ref={ref}
33
+ role="status"
34
+ className={infoBannerVariants({ variant, className })}
35
+ {...props}
36
+ >
37
+ <div className="flex gap-2.5 items-start">{children}</div>
38
+ </div>
39
+ );
40
+ }
41
+ );
42
+
43
+ InfoBanner.displayName = "InfoBanner";
44
+
45
+ export const InfoBannerIcon = React.forwardRef<
46
+ HTMLDivElement,
47
+ React.HTMLAttributes<HTMLDivElement>
48
+ >(({ className, children, ...props }, ref) => (
49
+ <div
50
+ ref={ref}
51
+ className={`flex-shrink-0 mt-0.5 opacity-70 ${className || ""}`}
52
+ {...props}
53
+ >
54
+ {children}
55
+ </div>
56
+ ));
57
+
58
+ InfoBannerIcon.displayName = "InfoBannerIcon";
59
+
60
+ export const InfoBannerContent = React.forwardRef<
61
+ HTMLDivElement,
62
+ React.HTMLAttributes<HTMLDivElement>
63
+ >(({ className, ...props }, ref) => (
64
+ <div
65
+ ref={ref}
66
+ className={`flex-1 space-y-0.5 ${className || ""}`}
67
+ {...props}
68
+ />
69
+ ));
70
+
71
+ InfoBannerContent.displayName = "InfoBannerContent";
72
+
73
+ export const InfoBannerTitle = React.forwardRef<
74
+ HTMLHeadingElement,
75
+ React.HTMLAttributes<HTMLHeadingElement>
76
+ >(({ className, ...props }, ref) => (
77
+ <h6
78
+ ref={ref}
79
+ className={`font-medium text-sm leading-tight ${className || ""}`}
80
+ {...props}
81
+ />
82
+ ));
83
+
84
+ InfoBannerTitle.displayName = "InfoBannerTitle";
85
+
86
+ export const InfoBannerDescription = React.forwardRef<
87
+ HTMLParagraphElement,
88
+ React.HTMLAttributes<HTMLParagraphElement>
89
+ >(({ className, ...props }, ref) => (
90
+ <div
91
+ ref={ref}
92
+ className={`text-sm leading-relaxed opacity-90 ${className || ""}`}
93
+ {...props}
94
+ />
95
+ ));
96
+
97
+ InfoBannerDescription.displayName = "InfoBannerDescription";
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const Input = React.forwardRef<
5
+ HTMLInputElement,
6
+ React.InputHTMLAttributes<HTMLInputElement>
7
+ >(({ className, type, ...props }, ref) => {
8
+ return (
9
+ <input
10
+ type={type}
11
+ className={cn(
12
+ "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent disabled:cursor-not-allowed disabled:opacity-50",
13
+ className
14
+ )}
15
+ ref={ref}
16
+ {...props}
17
+ />
18
+ );
19
+ });
20
+ Input.displayName = "Input";
@@ -0,0 +1,17 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ export const Label = React.forwardRef<
5
+ HTMLLabelElement,
6
+ React.LabelHTMLAttributes<HTMLLabelElement>
7
+ >(({ className, ...props }, ref) => (
8
+ <label
9
+ ref={ref}
10
+ className={cn(
11
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-foreground",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ ));
17
+ Label.displayName = "Label";
@@ -0,0 +1,29 @@
1
+ import React from "react";
2
+ import { cn } from "../utils";
3
+
4
+ interface LoadingSpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
5
+ size?: "sm" | "md" | "lg";
6
+ }
7
+
8
+ export const LoadingSpinner: React.FC<LoadingSpinnerProps> = ({
9
+ size = "md",
10
+ className,
11
+ ...props
12
+ }) => {
13
+ const sizeClasses = {
14
+ sm: "w-4 h-4 border-2",
15
+ md: "w-8 h-8 border-4",
16
+ lg: "w-12 h-12 border-4",
17
+ };
18
+
19
+ return (
20
+ <div className={cn("flex justify-center py-12", className)} {...props}>
21
+ <div
22
+ className={cn(
23
+ "border-indigo-200 border-t-indigo-500 rounded-full animate-spin",
24
+ sizeClasses[size]
25
+ )}
26
+ />
27
+ </div>
28
+ );
29
+ };